Back to Articles

Building Threat Intelligence Tools in Go with Passive DNS Lookups

[ View on GitHub ]

Building Threat Intelligence Tools in Go with Passive DNS Lookups

Hook

Every DNS query you've ever made—and millions of others—has been quietly recorded. Passive DNS databases like DNSDB store years of this historical data, letting security researchers trace how malicious domains evolve, pivot between infrastructure, and hide in plain sight.

Context

When investigating security incidents or tracking threat actors, real-time DNS lookups only tell you what exists right now. But attackers don't stand still—they rotate IP addresses, swap name servers, and burn through domains faster than you can query them. Passive DNS solves this by collecting and storing historical DNS resolution data from recursive resolvers worldwide. Instead of asking "where does evil.com point today?" you can ask "where has evil.com pointed over the last five years?" This historical view is invaluable for incident response, threat hunting, and understanding attacker infrastructure.

Farsight Security's DNSDB is one of the largest commercial passive DNS databases, aggregating billions of DNS records from a global sensor network. While they provide a REST API, working directly with HTTP endpoints means writing boilerplate authentication code, parsing JSON responses into usable structures, and handling pagination. The go-dnsdb library bridges this gap for Go developers, providing an idiomatic client that handles the plumbing so you can focus on threat analysis logic. It's unofficial—meaning Farsight doesn't maintain it—but follows patterns familiar to anyone who's used official Go clients from Google or GitHub.

Technical Insight

Transport Layer

Authentication Layer

LookupRRSet request

HTTP request

Inject X-API-Key header

Authenticated request

JSON response

Raw response

Parse & map

Typed records

User Application

DNSDB Client

APIKey Transport

Middleware

HTTP Client

Farsight DNSDB API

Typed Go Structs

RRSet, Metadata

System architecture — auto-generated

The library's architecture revolves around a clean separation of concerns: authentication lives in an HTTP transport wrapper, API interaction happens through a dedicated client struct, and responses map to strongly-typed Go structs. This design pattern—common in Go's ecosystem—makes the client both flexible and testable.

Authentication uses the transport middleware pattern via APIKeyTransport. Instead of manually adding API keys to every request, you wrap the base HTTP transport with a custom RoundTripper that injects the X-API-Key header automatically:

package main

import (
    "fmt"
    "net/http"
    "time"
    
    "github.com/bored-engineer/go-dnsdb"
)

func main() {
    // Create client with API key authentication
    client := dnsdb.NewClient("your-api-key-here", nil)
    
    // Look up historical A records for a domain
    rrsets, err := client.LookupRRSet("example.com", "A")
    if err != nil {
        panic(err)
    }
    
    // Iterate through historical records
    for _, rr := range rrsets {
        fmt.Printf("First seen: %s\n", time.Unix(rr.TimeFirst, 0))
        fmt.Printf("Last seen: %s\n", time.Unix(rr.TimeLast, 0))
        fmt.Printf("RData: %v\n", rr.RData)
        fmt.Println("---")
    }
}

This approach keeps authentication concerns isolated. If you need to customize the HTTP client—adding timeouts, proxies, or retry logic—you pass your configured http.Client as the second parameter to NewClient(). The library will wrap your transport with its authentication layer without clobbering your settings. This is more elegant than libraries that force you to set authentication on every method call or configure globally through package-level variables.

The response structures use Go's native types effectively. Timestamps come as Unix epochs (int64) rather than strings, making temporal analysis straightforward without parsing overhead. DNS records arrive as slices of strings for multi-value responses (like multiple A records pointing to different IPs), matching how DNS actually works:

type RRSet struct {
    RRName    string   `json:"rrname"`
    RRType    string   `json:"rrtype"`
    RData     []string `json:"rdata"`
    TimeFirst int64    `json:"time_first"`
    TimeLast  int64    `json:"time_last"`
    Count     int      `json:"count"`
}

For threat intelligence workflows, this structure maps cleanly to time-series analysis. You might build a tool that flags domains showing rapid IP rotation—a common indicator of compromised infrastructure or DGA (domain generation algorithm) activity. The TimeFirst and TimeLast fields give you the observation window, while Count indicates how frequently the record appeared in DNSDB's sensors.

The library also supports inverse lookups—finding all domains that resolved to a specific IP address. This "pivoting" capability is crucial for threat hunting. If you identify one malicious domain, you can find other domains sharing the same infrastructure:

// Find all domains that pointed to this IP
domains, err := client.LookupRData("192.0.2.1", "A")
if err != nil {
    panic(err)
}

// Analyze the neighborhood
for _, domain := range domains {
    fmt.Printf("Domain: %s (seen %d times)\n", domain.RRName, domain.Count)
}

This inverse mapping turns DNSDB into a graph database where you can traverse connections between domains, IPs, and name servers. Security teams use this to map out entire threat actor infrastructures, identifying patterns in how attackers organize their operations.

Gotcha

The elephant in the room: you can't actually use this library without a DNSDB subscription, which starts at hundreds of dollars monthly for API access. There's no free tier, no sandbox environment, and no sample data to experiment with. This makes go-dnsdb essentially unusable for learning, side projects, or proving value before budget approval. The library itself might be open source, but the data it accesses is locked behind a paywall that puts it firmly in enterprise territory.

Beyond access costs, the unofficial status carries real risks. With only five GitHub stars and no indication of active maintenance, you're inheriting technical debt if you depend on this in production. When Farsight updates their API—and they do, occasionally adding rate limit headers or deprecating endpoints—there's no guarantee this library will keep pace. The truncated README documentation suggests the author built what they needed and moved on, which is fine for personal tooling but problematic for team dependencies. You'll likely need to read the DNSDB API documentation directly and possibly extend the library yourself for advanced features like filtering, pagination controls, or the newer Flex Search endpoints. If your security tooling depends on passive DNS and you choose this library, budget time to fork it and maintain your own version.

Verdict

Use if: You're already a Farsight DNSDB customer building Go-based security tools (SIEM integrations, threat intelligence platforms, automated investigation workflows) and need a quick, idiomatic way to query passive DNS without writing HTTP client code from scratch. The transport middleware pattern and typed responses will save you boilerplate and integrate cleanly with existing Go security infrastructure. Skip if: You don't have DNSDB access and can't justify the subscription cost, you need battle-tested production reliability with active maintenance and comprehensive documentation, or you're working with multiple passive DNS providers and need abstraction across data sources. In those cases, either use Farsight's official Python client (if you can work in Python), build a thin wrapper around their REST API directly using Go's standard library, or investigate alternative passive DNS services like SecurityTrails that offer official Go SDKs and lower entry costs for smaller teams.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/developer-tools/bored-engineer-go-dnsdb.svg)](https://starlog.is/api/badge-click/developer-tools/bored-engineer-go-dnsdb)