12

What are the ways to Check SPF records on a domain?

There is a website where i can do it manually using - http://www.mxtoolbox.com/SuperTool.aspx

How can i do it via ASP.NET and C#? Basically i want to verify/check SPF records on a domain if its supporting out own mail web server.

Cristian Lupascu
  • 39,078
  • 16
  • 100
  • 137
nimi
  • 5,359
  • 16
  • 58
  • 90

9 Answers9

15

I have the same problem, and managed to find two three solutions:

The nslookup solution

You can get the SPF by typing the following command in the command line:

nslookup -type=TXT <hostname>

You can automate this in C# using System.Diagonstics.Process, as described in this blog post.

The DNS.NET Resolver project

I found this CodeProject article about DNS resolution. It comes with demo project. I ran the project and got the following result for stackexchange.com:

DNS Dig screeenshot

Note: Make sure that the QType field is set to TXT before you press the Send button

The section highlighted in yellow represents the SPF record. I haven't yet dug into the code to see how it's done, but this seems like a good alternative to the nslookup solution above.

[Update] The ARSoft.Tools.Net project

If all you need to do is check whether a domain supports a mail server, you can use the ARSoft.Tools.Net library (also available as a NuGet Package).

After installing the package, I managed to perform the SPF check with this code:

var spfValidator = new ARSoft.Tools.Net.Spf.SpfValidator();

var mailIpAddress = IPAddress.Parse("X.X.X.X");
var domain = "example.com";
var senderAddress = "sender@example.com";

ARSoft.Tools.Net.Spf.SpfQualifier result = 
    spfValidator.CheckHost(mailIpAddress, domain, senderAddress);
Cristian Lupascu
  • 39,078
  • 16
  • 100
  • 137
  • Hi W0lf, what does "X.X.X.X" in IPAddress.Parse(), stands for? – nimi Aug 31 '12 at 11:09
  • This is working now but i have 4 email webserver's and i wanna test either of these are trusted by the other companies to send email's on behalf of our webserver. How can i do this? Let me know your thoughts on this. – nimi Aug 31 '12 at 11:31
  • @nimi the `X.X.X.X` is a placeholder for the real IP you want to test. – Cristian Lupascu Sep 01 '12 at 12:34
  • @nimi if you have multiple Mail Servers (meaning more IPs to test), simply iterate through the available IPs and run the above check for each of them. – Cristian Lupascu Sep 01 '12 at 12:36
  • I have created a sample project and is working fine. But when i add the same in my project, i am getting some wiered error, ARSoft.Tools.Net.Spf.SpfQualifier.TempError. I dont understand why i am getting tis error. Could you please let me know what could be the issue. I have used the above code. – nimi Sep 11 '12 at 04:46
  • @nimi I'm not sure what it means either, except that an error occurred while performing the check (RFC docs link: http://tools.ietf.org/html/rfc4408#section-2.5.6). You might want to retry - there might have been a network/DNS error – Cristian Lupascu Sep 11 '12 at 06:38
  • Really nice. What about DKIM validation. Can we use same library for check Dkim ? – Naveen Jan 29 '18 at 13:30
6

Even though .NET has a lot of support for networking including doing host name to address mapping it lacks a general way to query DNS.

However, you can use P/Invoke to call the DnsQuery function directly. The API is somewhat cumbersome but it is not impossible to create the correct P/Invoke signature for your requirement.

A SPF record is stored as a TXT record in DNS. The corresponding structure you will have to work with is the DNS_TXT_DATA structure. If you can find an example of querying a MX record you can reuse the code and use DNS_TYPE_TEXT for the query type and unmarshal the data to a DNS_TXT_DATA structure.

Or you could just use this code:

using System.ComponentModel;
using System.Runtime.InteropServices;

public String DnsGetTxtRecord(String name) {
  const Int16 DNS_TYPE_TEXT = 0x0010;
  const Int32 DNS_QUERY_STANDARD = 0x00000000;
  const Int32 DNS_ERROR_RCODE_NAME_ERROR = 9003;
  const Int32 DNS_INFO_NO_RECORDS = 9501;
  var queryResultsSet = IntPtr.Zero;
  try {
    var dnsStatus = DnsQuery(
      name,
      DNS_TYPE_TEXT,
      DNS_QUERY_STANDARD,
      IntPtr.Zero,
      ref queryResultsSet,
      IntPtr.Zero
    );
    if (dnsStatus == DNS_ERROR_RCODE_NAME_ERROR || dnsStatus == DNS_INFO_NO_RECORDS)
      return null;
    if (dnsStatus != 0)
      throw new Win32Exception(dnsStatus);
    DnsRecordTxt dnsRecord;
    for (var pointer = queryResultsSet; pointer != IntPtr.Zero; pointer = dnsRecord.pNext) {
      dnsRecord = (DnsRecordTxt) Marshal.PtrToStructure(pointer, typeof(DnsRecordTxt));
      if (dnsRecord.wType == DNS_TYPE_TEXT) {
        var lines = new List<String>();
        var stringArrayPointer = pointer
          + Marshal.OffsetOf(typeof(DnsRecordTxt), "pStringArray").ToInt32();
        for (var i = 0; i < dnsRecord.dwStringCount; ++i) {
          var stringPointer = (IntPtr) Marshal.PtrToStructure(stringArrayPointer, typeof(IntPtr));
          lines.Add(Marshal.PtrToStringUni(stringPointer));
          stringArrayPointer += IntPtr.Size;
        }
        return String.Join(Environment.NewLine, lines);
      }
    }
    return null;
  }
  finally {
    const Int32 DnsFreeRecordList = 1;
    if (queryResultsSet != IntPtr.Zero)
      DnsRecordListFree(queryResultsSet, DnsFreeRecordList);
  }
}

[DllImport("Dnsapi.dll", EntryPoint = "DnsQuery_W", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
static extern Int32 DnsQuery(String lpstrName, Int16 wType, Int32 options, IntPtr pExtra, ref IntPtr ppQueryResultsSet, IntPtr pReserved);

[DllImport("Dnsapi.dll")]
static extern void DnsRecordListFree(IntPtr pRecordList, Int32 freeType);

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct DnsRecordTxt {
  public IntPtr pNext;
  public String pName;
  public Int16 wType;
  public Int16 wDataLength;
  public Int32 flags;
  public Int32 dwTtl;
  public Int32 dwReserved;
  public Int32 dwStringCount;
  public String pStringArray;
}
Martin Liversage
  • 104,481
  • 22
  • 209
  • 256
  • +1 This is great! I've tested your code and it works fine. However, in my case I'm still going to use the ARSoft.Tools.Net because it also does the parsing/validation. – Cristian Lupascu Aug 11 '12 at 18:37
  • +1 Hi Martin. Thanks! Very helpful. But there is a small bug in it. Long TXT records (dnsRecord.dwStringCount > 1) will result in multiple records (aka lines) in your code. But its easy to fix. So thanks again. – Tobias J. Aug 30 '13 at 14:53
1

Building on the answer by Martin Liversage, I've added some comments that explain what's going on, and adjusted to return multiple records if they exist.

My example also concatenates multiple strings in a TXT record rather than separating by line breaks.

I don't know if the if (dnsRecord.wType == DNS_TYPE_TEXT) line is really necessary considering that constraint is in the arguments to the DnsQuery function, but I preserved it from Martin's answer anyway.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;

namespace Util
{
    /// <summary>
    /// Based on https://stackoverflow.com/a/11884174 (Martin Liversage)
    /// </summary>
    class DnsInterop
    {
        private const short DNS_TYPE_TEXT = 0x0010;
        private const int DNS_QUERY_STANDARD = 0x00000000;
        private const int DNS_ERROR_RCODE_NAME_ERROR = 9003;
        private const int DNS_INFO_NO_RECORDS = 9501;


        public static IEnumerable<string> GetTxtRecords(string domain)
        {
            var results = new List<string>();
            var queryResultsSet = IntPtr.Zero;
            DnsRecordTxt dnsRecord;

            try
            {
                // get all text records
                // pointer to results is returned in queryResultsSet
                var dnsStatus = DnsQuery(
                  domain,
                  DNS_TYPE_TEXT,
                  DNS_QUERY_STANDARD,
                  IntPtr.Zero,
                  ref queryResultsSet,
                  IntPtr.Zero
                );

                // return null if no records or DNS lookup failed
                if (dnsStatus == DNS_ERROR_RCODE_NAME_ERROR
                    || dnsStatus == DNS_INFO_NO_RECORDS)
                {
                    return null;
                }

                // throw an exception if other non success code
                if (dnsStatus != 0)
                    throw new Win32Exception(dnsStatus);

                // step through each result
                for (
                    var pointer = queryResultsSet; 
                    pointer != IntPtr.Zero; 
                    pointer = dnsRecord.pNext)
                {
                    dnsRecord = (DnsRecordTxt)
                        Marshal.PtrToStructure(pointer, typeof(DnsRecordTxt));

                    if (dnsRecord.wType == DNS_TYPE_TEXT)
                    {
                        var builder = new StringBuilder();

                        // pointer to array of pointers
                        // to each string that makes up the record
                        var stringArrayPointer = pointer + Marshal.OffsetOf(
                            typeof(DnsRecordTxt), "pStringArray").ToInt32();

                        // concatenate multiple strings in the case of long records
                        for (var i = 0; i < dnsRecord.dwStringCount; ++i)
                        {
                            var stringPointer = (IntPtr)Marshal.PtrToStructure(
                                stringArrayPointer, typeof(IntPtr));

                            builder.Append(Marshal.PtrToStringUni(stringPointer));
                            stringArrayPointer += IntPtr.Size;
                        }

                        results.Add(builder.ToString());
                    }
                }
            }
            finally
            {
                if (queryResultsSet != IntPtr.Zero)
                {
                    DnsRecordListFree(queryResultsSet, 
                        (int)DNS_FREE_TYPE.DnsFreeRecordList);
                }
            }

            return results;
        }


        [DllImport("Dnsapi.dll", EntryPoint = "DnsQuery_W", 
            ExactSpelling = true, CharSet = CharSet.Unicode, 
            SetLastError = true)]
        static extern int DnsQuery(string lpstrName, short wType, int options, 
            IntPtr pExtra, ref IntPtr ppQueryResultsSet, IntPtr pReserved);


        [DllImport("Dnsapi.dll")]
        static extern void DnsRecordListFree(IntPtr pRecordList, int freeType);


        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
        struct DnsRecordTxt
        {
            public IntPtr pNext;
            public string pName;
            public short wType;
            public short wDataLength;
            public int flags;
            public int dwTtl;
            public int dwReserved;
            public int dwStringCount;
            public string pStringArray;
        }


        enum DNS_FREE_TYPE
        {
            DnsFreeFlat = 0,
            DnsFreeRecordList = 1,
            DnsFreeParsedMessageFields = 2
        }
    }
}
Community
  • 1
  • 1
Gordon Leigh
  • 1,263
  • 2
  • 11
  • 23
0

You basically need to do a DNS request asking for the MX/SPF record of the domain. There's a few examples of doing this in C# around. There's a library at http://mailsystem.codeplex.com/ that has a Validator class with GetMxRecords to do this that you might find useful

Peter Ritchie
  • 35,463
  • 9
  • 80
  • 98
  • I wanted to get SPF records, not MX Records. MX records does not give what i want, i want SPF records. – nimi Aug 09 '12 at 12:24
  • @nimi You could have asked that in the original question instead of completely re-writing the question. SPF is just a DNS record, you should be able to query it the same as an SPF record. – Peter Ritchie Aug 09 '12 at 15:29
0

For what is worth - MailBee's .NET Objects support this as well. I only say this because we already owned this component and I was about to implement something else when I found this goodie already baked into what we had.

http://www.afterlogic.com/mailbee-net/docs/filter_spam_with_dns.html

ProVega
  • 5,864
  • 2
  • 36
  • 34
0

We tried to use @martin-liversage's answer, but after some time running on hundrets of domains it failed on some memory problem. (Maybe there was some invalid/another type DNS record?) So i studied this exact WINAPI functions and structures used in this case and edited the solution acordingly.

Links to WINAPI documentation are included in code.

So here's our improved code, that's working 100% even in our case:

public String GetSpfRecord(String domain)
{
    // Definition of DNS params
    const Int16 DNS_TYPE_TXT = 0x0010;
    const Int32 DNS_QUERY_STANDARD = 0x00000001;
    const Int32 DNS_ERROR_RCODE_NAME_ERROR = 9003;
    const Int32 DNS_INFO_NO_RECORDS = 9501;

    DnsRecordA dnsRecord;
    var queryResultsSet = IntPtr.Zero;

    try
    {
        var dnsStatus = DnsQuery(
          domain,
          DNS_TYPE_TXT,
          DNS_QUERY_STANDARD,
          IntPtr.Zero,
          ref queryResultsSet,
          IntPtr.Zero
        );

        if (dnsStatus == DNS_ERROR_RCODE_NAME_ERROR || dnsStatus == DNS_INFO_NO_RECORDS)
            return null;

        if (dnsStatus != 0)
            throw new Win32Exception(dnsStatus);

        for (IntPtr pointer = queryResultsSet; pointer != IntPtr.Zero; pointer = dnsRecord.pNext)
        {
            // Copies data from memory (size of DnsRecordA) from adress pointer to new alocated memory and creates instance of pointer to this place.
            dnsRecord = (DnsRecordA)Marshal.PtrToStructure(pointer, typeof(DnsRecordA));

            // pokud se jedná o typ TXT
            if (dnsRecord.wType == DNS_TYPE_TXT)
            {
                // get pointer to informations in "Data" property (https://learn.microsoft.com/en-us/windows/win32/api/windns/ns-windns-dns_recorda)
                var dataPointer = pointer + Marshal.SizeOf(typeof(DnsRecordA));

                // Get the txtData
                var txtData = (DNS_TXT_DATAA)Marshal.PtrToStructure(dataPointer, typeof(DNS_TXT_DATAA));
                if (txtData.dwStringCount >= 1)
                {
                    string line = Marshal.PtrToStringUni(txtData.pStringArray[0]);

                    // only if record starts with "v=spf" (Getting only SPF records)
                    // Getting only first (here is always maximum of 1 record) and returning whole line
                    if (line.StartsWith("v=spf") && string.IsNullOrEmpty(result))
                    {
                        return line;
                    }
                }
            }
        }

        // no SPF record - returning null
        return null;
    }
    finally
    {
        const Int32 DnsFreeRecordList = 1;

        // always release the memory alocated for list of dns records
        if (queryResultsSet != IntPtr.Zero)
            DnsRecordListFree(queryResultsSet, DnsFreeRecordList);
    }
}

// https://learn.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsquery_a
[DllImport("Dnsapi.dll", EntryPoint = "DnsQuery_W", ExactSpelling = true, CharSet = CharSet.Unicode, SetLastError = true)]
static extern Int32 DnsQuery(String lpstrName, Int16 wType, Int32 options, IntPtr pExtra, ref IntPtr ppQueryResultsSet, IntPtr pReserved);

// https://learn.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsrecordlistfree
[DllImport("Dnsapi.dll")]
static extern void DnsRecordListFree(IntPtr pRecordList, Int32 freeType);

// https://learn.microsoft.com/en-us/windows/win32/api/windns/ns-windns-dns_recorda
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct DnsRecordA
{
    public IntPtr pNext;
    public String pName;
    public Int16 wType;
    public Int16 wDataLength;
    public Int32 flags;
    public Int32 dwTtl;
    public Int32 dwReserved;

    // Commented, because i'm getting this value dynamicaly (it can also be another structure type which might cause some problems)
    //public DNS_TXT_DATA Data;
}

// https://learn.microsoft.com/en-us/windows/win32/api/windns/ns-windns-dns_txt_dataa
[StructLayout(LayoutKind.Sequential)]
struct DNS_TXT_DATAA
{
    /// DWORD->unsigned int
    public uint dwStringCount;

    /// PSTR[1]
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1, ArraySubType = UnmanagedType.SysUInt)]
    internal IntPtr[] pStringArray;
}
Gh61
  • 9,222
  • 4
  • 28
  • 39
0

Based on DnsDig project I created a DLL which can be used on any .net (vb , c#, forms, web. etc..) project

https://devselz.com/software/devselz_dnsdig_dns-txt-etc-query-domain-register.zip

Download the DLL , unzip, and Add as reference to your project (if website place on root/bin folder):

DnsDig.dll

nunit.framework.dll

(126KB in total)

Then use this example as for an ASP.Ne website (vb.net code)

Imports DnsDig
Imports Heijden.DNS
Partial Class lib_u_l_Default
    Inherits System.Web.UI.Page
    Public Resolver As Resolver
    Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
        Resolver = New Resolver
        Dim SW As New System.Diagnostics.Stopwatch
        SW.Start()
        Dim DNSResponse As Heijden.DNS.Response = Resolver.Query(Request.QueryString("d"), QType.SPF, QClass.ANY)
        SW.Stop()
        If DNSResponse.header.ANCOUNT > 0 Then
            For Each answerRR As AnswerRR In DNSResponse.Answers
                Response.Write("<br/>" & answerRR.ToString)
            Next
        End If
    End Sub

End Class

RESULTS: https://yourwebsiteusingabovedlls.com/anyplacewhereabovecode/?d=goodyes.com

will write

goodyes.com. 3535 IN TXT "google-site-verification=IMw-tL0VWgMJbtcRgt_bu5UaVwpbNb94dvcOSObooa4" goodyes.com. 3535 IN TXT "v=spf1 include:_spf.buzondecorreo.com ~all"

Devmyselz
  • 346
  • 3
  • 13
0

One option is to use the package DnsClient.NET (Github-Repo).

This library is able to do high performant DNS lookups as well as lookups for SPF-entries.

Example code for SPF-lookup:

var domainName = "example.domain";
var lookup = new LookupClient();
var result = await lookup.QueryAsync(domainName, QueryType.TXT).ConfigureAwait(false);

var records = result.Answers.OfType<TxtRecord>().ToList();
Matthias Müller
  • 444
  • 6
  • 15
-1

Funny how all the websites has this wrong

SPF is not TXT

you can have a txt record with no SPF and SPF with no TXT, so a TXT lookup will not show the SPF

Renier
  • 1
  • 2
    Not anymore. SPF type resource records were deprecated in [RFC7208](https://tools.ietf.org/html/rfc7208). See section 3.1 for the reasoning, but the TLDR is that nobody was adopting it because nobody had a mail server that would do SPF type DNS requests. Everybody implemented TXT only. – Bacon Bits Aug 08 '17 at 18:39