Skip to content

A Go SDK for checking whether domains are blocked by Indonesian ISP DNS filters (Nawala/Kominfo (now Komdigi)). It works by querying configurable DNS servers and scanning the responses for blocking keywords such as internetpositif.id redirects or trustpositif.komdigi.go.id EDE indicators.

License

Notifications You must be signed in to change notification settings

H0llyW00dzZ/nawala-checker

🌐 Nawala Checker

Go Version Go Reference Go Report Card License codecov Ask DeepWiki Baca dalam Bahasa Indonesia

A Go SDK for checking whether domains are blocked by Indonesian ISP DNS filters (Nawala/Kominfo (now Komdigi)). It works by querying configurable DNS servers and scanning the responses for blocking keywords such as internetpositif.id redirects or trustpositif.komdigi.go.id EDE indicators.

Important

This SDK requires an Indonesian network to function correctly. Nawala DNS servers only return blocking responses when queried from within Indonesia. Ensure your connection uses a pure Indonesian IP with no routing through networks outside Indonesia.

Note

This SDK is not deprecated or outdated. Despite rumors that the original Nawala project may cease operations, this module remains a general-purpose DNS checking toolkit built from the ground up with customizable DNS server and client configurations. You can point it at any DNS server, define your own blocking keywords, and plug in custom *dns.Client instances (TCP, DNS-over-TLS, custom dialers, etc.). The default Nawala servers are simply pre-configured defaults; the SDK itself is fully independent and actively maintained.

Tip

When running on cloud infrastructure (e.g., VPS, microservices, k8s) that is not on an Indonesian network (e.g., a Singapore or US server), implement your own DNS server on an Indonesian network, then point this SDK at it using WithServers. Block indicator behavior depends on the DNS server in use; the default Nawala/Komdigi servers only return block indicators when queried from an Indonesian source IP.

Features

  • Concurrent domain checking — check multiple domains in parallel with a single call
  • DNS server failover — automatic fallback to secondary servers when the primary fails
  • Retry with exponential backoff — resilient against transient network errors
  • Built-in caching — in-memory cache with configurable TTL to avoid redundant queries
  • Custom cache backends — plug in Redis, memcached, or any backend via the Cache interface
  • Server health checks — monitor online/offline status and latency of DNS servers
  • Panic recovery — goroutines are protected from panics with automatic recovery and typed errors
  • Functional options — clean, idiomatic Go configuration pattern
  • Context-aware — full support for timeouts and cancellation via context.Context
  • Domain validation — automatic normalization and validation of domain names
  • Typed errors — sentinel errors for errors.Is matching (see Errors)

Installation

go get github.com/H0llyW00dzZ/nawala-checker

Requires Go 1.25.6 or later.

Quick Start

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/H0llyW00dzZ/nawala-checker/src/nawala"
)

func main() {
    c := nawala.New()

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    results, err := c.Check(ctx, "google.com", "reddit.com", "github.com", "exam_ple.com")
    if err != nil {
        log.Fatal(err)
    }

    for _, r := range results {
        status := "not blocked"
        if r.Blocked {
            status = "BLOCKED"
        }
        if r.Error != nil {
            status = fmt.Sprintf("error: %v", r.Error)
        }
        fmt.Printf("%-20s %s (server: %s)\n", r.Domain, status, r.Server)
    }
}

Configuration

Use functional options to customize the checker:

c := nawala.New(
    // Increase timeout for slow networks.
    nawala.WithTimeout(15 * time.Second),

    // Allow more retries (3 retries = 4 total attempts).
    nawala.WithMaxRetries(3),

    // Cache results for 10 minutes.
    nawala.WithCacheTTL(10 * time.Minute),

    // Replace all DNS servers with custom ones.
    nawala.WithServers([]nawala.DNSServer{
        {
            Address:   "8.8.8.8",
            Keyword:   "blocked",
            QueryType: "A",
        },
        {
            Address:   "8.8.4.4",
            Keyword:   "blocked",
            QueryType: "A",
        },
    }),

    // Limit concurrent checks to 50 goroutines.
    nawala.WithConcurrency(50),

    // Use a custom DNS client (e.g., for TCP or DNS-over-TLS).
    nawala.WithDNSClient(&dns.Client{
        Timeout: 10 * time.Second,
        Net:     "tcp-tls",
    }),

    // Set custom EDNS0 size (default is 1232 to prevent fragmentation).
    nawala.WithEDNS0Size(4096),
)

Available Options

Option Default Description
WithTimeout(d) 5s Timeout for each DNS query
WithMaxRetries(n) 2 Max retry attempts per query (total = n+1)
WithCacheTTL(d) 5m TTL for the built-in in-memory cache
WithCache(c) in-memory Custom Cache implementation (pass nil to disable)
WithConcurrency(n) 100 Max concurrent DNS checks (semaphore size)
WithEDNS0Size(n) 1232 EDNS0 UDP buffer size (prevents fragmentation)
WithDNSClient(c) UDP client Custom *dns.Client for TCP, TLS, or custom dialer
WithServer(s) Deprecated: use Checker.SetServers. Add or replace a single server
WithServers(s) Nawala defaults Replace all DNS servers
Checker.SetServers(s) Hot-reload: Add or replace servers at runtime safely
Checker.HasServer(s) Hot-reload: Check if a server is configured at runtime safely
Checker.DeleteServers(s) Hot-reload: Remove servers at runtime safely

API

Core Methods

// Check multiple domains concurrently.
results, err := c.Check(ctx, "example.com", "another.com")

// Check a single domain.
result, err := c.CheckOne(ctx, "example.com")

// Check DNS server health and latency.
statuses, err := c.DNSStatus(ctx)

// Clear the result cache.
c.FlushCache()

// Get configured servers.
servers := c.Servers()

// Hot-reload: Add or replace servers at runtime (concurrency-safe).
c.SetServers(nawala.DNSServer{
    Address:   "203.0.113.1",
    Keyword:   "blocked",
    QueryType: "A",
})

// Hot-reload: Check if a server is currently configured.
if c.HasServer("203.0.113.1") {
    fmt.Println("Server is active")
}

// Hot-reload: Remove servers at runtime by IP address (concurrency-safe).
c.DeleteServers("203.0.113.1")

Validation

// Validate a domain name before checking.
ok := nawala.IsValidDomain("example.com") // true
ok  = nawala.IsValidDomain("invalid")     // false (single label, no TLD)

Types

// Result of checking a single domain.
type Result struct {
    Domain  string  // The domain that was checked
    Blocked bool    // Whether the domain is blocked
    Server  string  // DNS server IP used for the check
    Error   error   // Non-nil if the check failed
}

// Health status of a DNS server.
type ServerStatus struct {
    Server    string  // DNS server IP address
    Online    bool    // Whether the server is responding
    LatencyMs int64   // Round-trip time in milliseconds
    Error     error   // Non-nil if the health check failed
}

// DNS server configuration.
type DNSServer struct {
    Address   string  // IP address of the DNS server
    Keyword   string  // Blocking keyword to search for in responses
    QueryType string  // DNS record type: "A", "AAAA", "CNAME", "TXT", etc.
}

Errors

var (
    ErrNoDNSServers  // No DNS servers configured
    ErrAllDNSFailed  // All DNS servers failed to respond
    ErrInvalidDomain // Domain name failed validation
    ErrDNSTimeout    // DNS query exceeded the configured timeout
    ErrInternalPanic // An internal panic was recovered during execution
    ErrNXDOMAIN      // Domain does not exist (NXDOMAIN)
    ErrQueryRejected // Query explicitly rejected by server (Format Error, Refused, Not Implemented)
)

Custom Cache

Implement the Cache interface to use a custom cache backend:

type Cache interface {
    Get(key string) (Result, bool)
    Set(key string, val Result)
    Flush()
}

Examples

Runnable examples are available in the examples/ directory:

Example Description
basic Check multiple domains with default configuration
custom Advanced configuration with custom servers, timeouts, retries, and caching
status Monitor DNS server health and latency
hotreload Hot-reload DNS servers at runtime

Run an example (requires cloning the repository):

git clone https://github.com/H0llyW00dzZ/nawala-checker.git
cd nawala-checker
go run ./examples/basic

Default DNS Servers

The checker comes pre-configured with known Nawala DNS servers:

Server Keyword Query Type
180.131.144.144 internetpositif A
180.131.145.145 internetpositif A

Nawala blocks domains by returning CNAME redirects to known block pages (internetpositif.id or internetsehatku.com). Komdigi blocks domains by returning an A record with EDE 15 (Blocked) containing trustpositif.komdigi.go.id. The keyword is matched against the full DNS record string for broad detection.

How Blocking Works

Indonesian ISP DNS filters use two distinct blocking mechanisms:

Nawala — CNAME Redirect

Nawala intercepts DNS queries for blocked domains and returns a CNAME redirect to a landing page instead of the real IP address:

;; ANSWER SECTION:
blocked.example.    3600    IN    CNAME    internetpositif.id.

The checker detects this by scanning all DNS record sections (Answer, Authority, Additional) for the keyword internetpositif in any record's string representation.

Komdigi — EDE 15 (Blocked)

Komdigi uses the newer Extended DNS Errors mechanism (RFC 8914). The response returns an A record pointing to a block page IP, along with an EDE option code 15 (Blocked) in the OPT pseudo-section:

;; OPT PSEUDOSECTION:
; EDE: 15 (Blocked): (source=block-list-zone;
;   blockListUrl=https://trustpositif.komdigi.go.id/assets/db/domains_isp;
;   domain=reddit.com)

;; ANSWER SECTION:
reddit.com.    30    IN    A    103.155.26.29

The checker detects this by scanning the Extra section (which contains the OPT record) for the keyword trustpositif or komdigi. To use this detection, configure a server with the appropriate keyword:

nawala.WithServers([]nawala.DNSServer{
    {
        Address:   "103.155.26.28",
        Keyword:   "trustpositif",
        QueryType: "A",
    },
    {
        Address:   "103.155.26.29",
        Keyword:   "komdigi",
        QueryType: "A",
    },
})

📜 The Legend of Nawala

For many "old-school" Indonesian internet users (the warnet generation), DNS Nawala is a legendary name. Taking its name from an Old Javanese word meaning "letter" or "message", the Nawala Project began around 2007-2009 as an initiative by Indonesian internet activists. It was an independent, free DNS filtering service originally designed to filter negative content (pornography, gambling, and malware) to create a safe and healthy internet environment. Before the term Internet Positif became mainstream, if you couldn't access a site, chances are you were blocked by Nawala.

It became so ubiquitous in internet cafes (warnet) and early ISPs that circumventing Nawala via custom DNS servers (like Google's 8.8.8.8) became a rite of passage for Indonesian netizens. The project itself has since transformed, evolving from internet security into application development, and eventually focusing on social contribution through technology training and education (now known as Nawala Education).

Today, while the original Nawala DNS filtering might be historical, its legacy lives on. The Indonesian government (Kominfo, now Komdigi) adopted and expanded upon these concepts, evolving from the early CNAME redirects (internetpositif.id) to modern, standards-compliant Extended DNS Errors (trustpositif.komdigi.go.id). This SDK honors that history while providing a robust tool to navigate the modern Indonesian internet filtering landscape.

Project Structure

nawala-checker/
├── .github/            # CI workflows and Dependabot configuration
├── examples/           # Runnable usage examples (basic, custom, status)
├── Makefile            # Build and test shortcuts
└── src/
    └── nawala/          # Core SDK package (checker, cache, DNS, options, types)

Testing

Tests must be run from a cloned repository:

git clone https://github.com/H0llyW00dzZ/nawala-checker.git
cd nawala-checker

Then run the desired target:

# Run tests with race detector.
make test

# Run tests with verbose output.
make test-verbose

# Run tests with coverage report.
make test-cover

# Skip live DNS tests.
make test-short

Roadmap

  • Upgrade github.com/miekg/dns to v2 or use a modern alternative for improved networking performance and features, due to its implementation in Go and its high effectiveness for networking.
  • Implement a CLI version (bundled in this repository) for checking domains directly from the terminal without writing Go code.

License

BSD 3-Clause License — Copyright (c) 2026, H0llyW00dzZ

About

A Go SDK for checking whether domains are blocked by Indonesian ISP DNS filters (Nawala/Kominfo (now Komdigi)). It works by querying configurable DNS servers and scanning the responses for blocking keywords such as internetpositif.id redirects or trustpositif.komdigi.go.id EDE indicators.

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Contributors