<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ExamineIP</title>
    <description>The latest articles on DEV Community by ExamineIP (@examineip).</description>
    <link>https://dev.to/examineip</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3843535%2Fb6f0104f-5d42-4881-844b-bec8f61e5cda.png</url>
      <title>DEV Community: ExamineIP</title>
      <link>https://dev.to/examineip</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/examineip"/>
    <language>en</language>
    <item>
      <title>My SPF checker said GitHub was at 8 of 10 DNS lookups. The real number is 10.</title>
      <dc:creator>ExamineIP</dc:creator>
      <pubDate>Mon, 28 Sep 2026 05:52:50 +0000</pubDate>
      <link>https://dev.to/examineip/my-spf-checker-said-github-was-at-8-of-10-dns-lookups-the-real-number-is-10-b6e</link>
      <guid>https://dev.to/examineip/my-spf-checker-said-github-was-at-8-of-10-dns-lookups-the-real-number-is-10-b6e</guid>
      <description>&lt;p&gt;I run a small set of free network tools, one of which checks a domain's SPF, DKIM and DMARC. The first version I shipped told anyone who checked &lt;code&gt;github.com&lt;/code&gt; that GitHub's SPF record used &lt;strong&gt;8 of the 10 DNS lookups&lt;/strong&gt; it's allowed.&lt;/p&gt;

&lt;p&gt;That looks comfortable. It isn't true. GitHub is at &lt;strong&gt;exactly 10 of 10&lt;/strong&gt; — one more mail vendor and its SPF stops working.&lt;/p&gt;

&lt;p&gt;My checker was counting the wrong thing, and it's a mistake that's easy to make, so here is what the rule actually says and how to count it properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7208#section-4.6.4" rel="noopener noreferrer"&gt;RFC 7208 §4.6.4&lt;/a&gt; caps SPF evaluation at &lt;strong&gt;10 terms that cause DNS lookups&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Costs a lookup&lt;/th&gt;
&lt;th&gt;Free&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip4:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip6:&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;all&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ptr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exp=&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exists:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redirect=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Go over 10 and the result is &lt;strong&gt;PermError&lt;/strong&gt;. The SPF check can no longer pass, so under DMARC your mail now has to survive on DKIM alone.&lt;/p&gt;

&lt;p&gt;The part that's easy to miss is the word &lt;em&gt;evaluation&lt;/em&gt;. The limit isn't per record. It's per evaluation — and evaluating your record means evaluating every record you include, and every record &lt;em&gt;they&lt;/em&gt; include.&lt;/p&gt;

&lt;h2&gt;
  
  
  What my checker did
&lt;/h2&gt;

&lt;p&gt;It split the domain's own record into terms and counted the ones in the left-hand column. For GitHub, that's this record (as of 28 September 2026):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;v&lt;/span&gt;=&lt;span class="n"&gt;spf1&lt;/span&gt; &lt;span class="n"&gt;ip4&lt;/span&gt;:&lt;span class="m"&gt;192&lt;/span&gt;.&lt;span class="m"&gt;30&lt;/span&gt;.&lt;span class="m"&gt;252&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;/&lt;span class="m"&gt;22&lt;/span&gt;
  &lt;span class="n"&gt;include&lt;/span&gt;:&lt;span class="n"&gt;spf&lt;/span&gt;.&lt;span class="n"&gt;protection&lt;/span&gt;.&lt;span class="n"&gt;outlook&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
  &lt;span class="n"&gt;include&lt;/span&gt;:&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;netblocks&lt;/span&gt;.&lt;span class="n"&gt;google&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
  &lt;span class="n"&gt;include&lt;/span&gt;:&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;netblocks2&lt;/span&gt;.&lt;span class="n"&gt;google&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
  &lt;span class="n"&gt;include&lt;/span&gt;:&lt;span class="n"&gt;mail&lt;/span&gt;.&lt;span class="n"&gt;zendesk&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
  &lt;span class="n"&gt;include&lt;/span&gt;:&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="n"&gt;spf&lt;/span&gt;.&lt;span class="n"&gt;salesforce&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
  &lt;span class="n"&gt;include&lt;/span&gt;:&lt;span class="n"&gt;servers&lt;/span&gt;.&lt;span class="n"&gt;mcsv&lt;/span&gt;.&lt;span class="n"&gt;net&lt;/span&gt;
  &lt;span class="n"&gt;include&lt;/span&gt;:&lt;span class="n"&gt;mktomail&lt;/span&gt;.&lt;span class="n"&gt;com&lt;/span&gt;
  &lt;span class="n"&gt;include&lt;/span&gt;:&lt;span class="n"&gt;sendgrid&lt;/span&gt;.&lt;span class="n"&gt;net&lt;/span&gt;
  &lt;span class="n"&gt;ip4&lt;/span&gt;:&lt;span class="m"&gt;62&lt;/span&gt;.&lt;span class="m"&gt;253&lt;/span&gt;.&lt;span class="m"&gt;227&lt;/span&gt;.&lt;span class="m"&gt;114&lt;/span&gt; &lt;span class="n"&gt;ip4&lt;/span&gt;:&lt;span class="m"&gt;166&lt;/span&gt;.&lt;span class="m"&gt;78&lt;/span&gt;.&lt;span class="m"&gt;69&lt;/span&gt;.&lt;span class="m"&gt;169&lt;/span&gt; &lt;span class="n"&gt;ip4&lt;/span&gt;:&lt;span class="m"&gt;166&lt;/span&gt;.&lt;span class="m"&gt;78&lt;/span&gt;.&lt;span class="m"&gt;69&lt;/span&gt;.&lt;span class="m"&gt;170&lt;/span&gt; &lt;span class="n"&gt;ip4&lt;/span&gt;:&lt;span class="m"&gt;166&lt;/span&gt;.&lt;span class="m"&gt;78&lt;/span&gt;.&lt;span class="m"&gt;71&lt;/span&gt;.&lt;span class="m"&gt;131&lt;/span&gt;
  ~&lt;span class="n"&gt;all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight &lt;code&gt;include:&lt;/code&gt;s. Eight lookups. My tool said "8 of 10" — and to be fair to past me, the help text underneath admitted it couldn't see inside the includes. But a number on a result card beats a caveat in a paragraph every time.&lt;/p&gt;

&lt;p&gt;Here's what's actually inside those eight includes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Include&lt;/th&gt;
&lt;th&gt;What its own record contains&lt;/th&gt;
&lt;th&gt;Extra lookups&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;spf.protection.outlook.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only &lt;code&gt;ip4:&lt;/code&gt; / &lt;code&gt;ip6:&lt;/code&gt; ranges&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_netblocks.google.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only &lt;code&gt;ip4:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_netblocks2.google.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only &lt;code&gt;ip6:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mail.zendesk.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only &lt;code&gt;ip4:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_spf.salesforce.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exists:%{i}._spf.mta.salesforce.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;servers.mcsv.net&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only &lt;code&gt;ip4:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mktomail.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only &lt;code&gt;ip4:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sendgrid.net&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ip4:&lt;/code&gt; ranges plus &lt;code&gt;include:ab.sendgrid.net&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;8 in GitHub's record + 1 hiding inside Salesforce's + 1 inside SendGrid's = &lt;strong&gt;10&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two things worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exists:&lt;/code&gt; counts&lt;/strong&gt;, even with a macro in it. &lt;code&gt;%{i}&lt;/code&gt; expands to the connecting IP, so Salesforce's record does a live DNS query per message. That's a lookup like any other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your count can change without you touching DNS.&lt;/strong&gt; GitHub's record didn't need to change for its total to grow — a vendor adding one &lt;code&gt;include:&lt;/code&gt; inside their own record is enough. The records you include are other people's infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Counting it properly
&lt;/h2&gt;

&lt;p&gt;Walk the tree. This is the whole idea in about 30 lines, using Google's DNS-over-HTTPS JSON API so it runs in Node 18+ (or a browser) without a DNS library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://dns.google/resolve?name=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;type=TXT`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Answer&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*"/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^"|"$/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;countLookups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`include loop: &lt;/span&gt;&lt;span class="p"&gt;${[...&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/^v=spf1&lt;/span&gt;&lt;span class="se"&gt;(\s&lt;/span&gt;&lt;span class="sr"&gt;|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has no SPF record (PermError if included)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; SPF records (PermError)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasAll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;~?&lt;/span&gt;&lt;span class="se"&gt;]?&lt;/span&gt;&lt;span class="sr"&gt;all$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\-&lt;/span&gt;&lt;span class="sr"&gt;~?&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;countLookups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirect=&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasAll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// redirect is ignored when "all" is present&lt;/span&gt;
      &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;countLookups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;a|mx|ptr&lt;/span&gt;&lt;span class="se"&gt;)([&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;/]&lt;/span&gt;&lt;span class="sr"&gt;|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;exists:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;                                  &lt;span class="c1"&gt;// ip4, ip6, all, exp= cost nothing&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;countLookups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;   &lt;span class="c1"&gt;// 10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details in there that are each a separate RFC rule, and each one a bug I've seen in real checkers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Loops are tracked per path, not globally.&lt;/strong&gt; Two different vendors can legitimately include the same third record; that's allowed and it simply costs twice. Only a record that includes itself (directly or further down) is a loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Including a domain with no SPF record is a PermError&lt;/strong&gt; (RFC 7208 §5.2), not a harmless skip. A vendor you stopped using, whose SPF record disappeared, breaks yours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two SPF records on one name is a PermError&lt;/strong&gt; (§4.5). Not "use the first one".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;redirect=&lt;/code&gt; is ignored if the record has an &lt;code&gt;all&lt;/code&gt;&lt;/strong&gt; (§6.1). Counting it anyway inflates the total.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this sketch leaves out, deliberately, to stay short:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The void-lookup limit.&lt;/strong&gt; Lookups that return nothing (NXDOMAIN or an empty answer) should be capped at &lt;strong&gt;two&lt;/strong&gt; (§4.6.4 — a SHOULD, but receivers that enforce it return PermError past that, even if you're under 10 overall).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each &lt;code&gt;mx&lt;/code&gt; gets its own sub-limit&lt;/strong&gt; of 10 address lookups for the MX hosts it returns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Macros&lt;/strong&gt; like &lt;code&gt;%{i}&lt;/code&gt; are counted, not expanded — which is correct for counting, since the lookup happens either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full version, with those rules and a test suite checked against real policies, is on GitHub as &lt;a href="https://github.com/examineip/dmarc-check" rel="noopener noreferrer"&gt;examineip/dmarc-check&lt;/a&gt; (MIT, one file, no dependencies).&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're at 9 or 10
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remove senders you no longer use.&lt;/strong&gt; Old marketing platforms are the usual suspects, and their includes are often the heaviest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move bulk senders to a subdomain.&lt;/strong&gt; &lt;code&gt;news.example.com&lt;/code&gt; gets its own SPF record and its own 10 lookups. DMARC's relaxed alignment still accepts it for &lt;code&gt;example.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flattening&lt;/strong&gt; (replacing an &lt;code&gt;include:&lt;/code&gt; with the IP ranges it currently resolves to) gets you under the limit today, but it freezes a vendor's ranges into your DNS. When they add a server, your mail from it starts failing, and nothing tells you why. Only flatten with automation that re-checks regularly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check yours
&lt;/h2&gt;

&lt;p&gt;You can run the fixed version against any domain in the browser at &lt;a href="https://tools.examineip.com/dmarc-checker/" rel="noopener noreferrer"&gt;ExamineIP's DMARC checker&lt;/a&gt;. It now reports GitHub as 10 of 10, with 8 in its own record — which is the honest answer, and a much more useful one.&lt;/p&gt;

&lt;p&gt;The lesson I took away is less about SPF than about tools in general: when a result is only an approximation, the approximation has to be in the number, not in the small print underneath it.&lt;/p&gt;

</description>
      <category>email</category>
      <category>dns</category>
      <category>security</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I built a simple IP checker tool</title>
      <dc:creator>ExamineIP</dc:creator>
      <pubDate>Wed, 25 Mar 2026 17:09:43 +0000</pubDate>
      <link>https://dev.to/examineip/i-built-a-simple-ip-checker-tool-5046</link>
      <guid>https://dev.to/examineip/i-built-a-simple-ip-checker-tool-5046</guid>
      <description>&lt;p&gt;I’ve been working on a simple IP checker that shows your IP address, location, ISP, and browser info.&lt;/p&gt;

&lt;p&gt;The idea was to keep it clean and easy to understand, especially for beginners who want to know what their IP actually reveals.&lt;/p&gt;

&lt;p&gt;While building it, I also realized how confusing the basics of the internet can be for most people — things like DNS, servers, and how a page actually loads.&lt;/p&gt;

&lt;p&gt;So I wrote a simple breakdown explaining how websites actually work, step by step.&lt;/p&gt;

&lt;p&gt;If you're curious, you can check it here:&lt;br&gt;
&lt;a href="https://examineip.com/how-websites-actually-work-simple-explanation-for-beginners/" rel="noopener noreferrer"&gt;https://examineip.com/how-websites-actually-work-simple-explanation-for-beginners/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And here’s the tool itself:&lt;br&gt;
&lt;a href="https://tools.examineip.com/" rel="noopener noreferrer"&gt;https://tools.examineip.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback or suggestions on what to improve.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>networking</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
