Skip to content

Repository files navigation

🌐 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
  • Streaming domain checking β€” process domains through a channel pipeline via CheckStream, enabling constant-memory operation even with millions of domains
  • 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
  • Cache key namespacing β€” all keys are prefixed with nawala_checker: to prevent collisions when sharing a cache backend between packages
  • Digest-based cache keys β€” optional WithDigests option to replace the plain key body with any hash (e.g. hex SHA-256 or Bitcoin-style double-SHA-256), producing nawala_checker:<digest>
  • 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)
  • Connection pooling β€” reuse established TCP/TLS connections via WithKeepAlive to eliminate handshake overhead
  • Runtime hot-reload β€” safely add, replace, or remove DNS servers concurrently without restarting
  • Agentic AI skills β€” ready-to-use SKILL.md definitions for seamless integration with opencode, openclaw, crush and other LLM agents (see skills/)

πŸš€ Performance

Because this SDK is written in Go and operates at the DNS protocol level (not over a REST API), it avoids the HTTP overhead of JSON serialization, TLS handshakes per request, and HTTP/2 multiplexing limits that affect API-based approaches. Every DNS query is a compact UDP or TCP packet β€” typically under 512 bytes β€” making the per-domain cost extremely low.

Concurrency is managed by a buffered channel semaphore (WithConcurrency, default 100). Each domain is dispatched to its own goroutine, and the semaphore ensures the system never spawns unbounded goroutines. This model scales linearly: raise the limit and the SDK uses more cores automatically.

Approach Protocol overhead Serialization Concurrency model
This SDK DNS over UDP/TCP (~64 B query) None β€” binary DNS wire format Goroutine per domain, semaphore-bounded
REST API checker HTTP/HTTPS + JSON JSON encode/decode per request Typically request-pool limited

In practice the SDK is capable of checking millions β€” or even billions β€” of domains in a single run. The upper bound is the DNS server's response rate, not the SDK itself:

  • Memory β€” each goroutine carries a small stack (~2–8 KB base); 100 concurrent goroutines β‰ˆ < 1 MB overhead
  • WithConcurrency(n) β€” raise n to match the server's capacity; lower it to be polite to shared resolvers
  • No hard domain-count limit β€” the SDK streams and dispatches domains on demand

Tip

For very large domain lists (millions to billions), combine a high WithConcurrency value with WithCache disabled (or a Redis-backed cache) and stream domains from a file via --file in the CLI.

πŸ“¦ Installation

go get github.com/H0llyW00dzZ/nawala-checker

Requires Go 1.25.6 or later.

πŸ’» CLI

Install the nawala command-line tool:

go install github.com/H0llyW00dzZ/nawala-checker/cmd/nawala@latest

Usage:

# Check domains (shorthand β€” delegates to "check")
nawala google.com reddit.com

# Check domains from a file (streamed line-by-line, constant memory)
nawala check --file domains.txt

# JSON output (NDJSON β€” one object per line)
nawala check google.com --format json

# Write results to a file (tab-separated text)
nawala check --file domains.txt -o results.txt

# Generate an HTML report
nawala check google.com reddit.com --format html -o report.html

# Generate an Excel spreadsheet (XLSX)
nawala check --file domains.txt --format xlsx -o results.xlsx

# Use a custom config (JSON or YAML)
nawala check --config config.json --file domains.txt

# Inspect effective configuration (show all defaults)
nawala config
nawala config --json

# Generate a config file
nawala config -o myconfig.json --json

# Show DNS server health and latency
nawala status

# Print version
nawala --version

Note

Domain input is case-insensitive β€” Google.com and google.com are treated as the same domain and deduplicated before checking. Unicode (IDN) domains are automatically converted to Punycode (ACE) using the IDNA Lookup profile (UTS#46), so you can pass δΎ‹γˆ.jp directly and it will be checked as xn--r8jz45g.jp.

Configuration file example (config.json) β€” nawala envelope format:

{
  "nawala": {
    "version": "0.6.5",
    "configuration": {
      "timeout": "5s",
      "command_timeout": "30s",
      "max_retries": 2,
      "cache_ttl": "5m",
      "disable_cache": false,
      "concurrency": 100,
      "edns0_size": 1232,
      "protocol": "udp",
      "tls_server_name": "",
      "tls_skip_verify": false,
      "keep_alive_pool_size": 0,
      "servers": [
        {"address": "180.131.144.144", "keyword": "internetpositif", "query_type": "A"},
        {"address": "103.155.26.28",  "keyword": "trustpositif",    "query_type": "A"}
      ]
    }
  }
}

Note

The version field records which CLI version generated the config. If it does not match the running CLI, a warning is printed to stderr and the config is still applied. Regenerate with nawala config --json -o config.json to update it.

Set disable_cache: true to disable the built-in in-memory cache entirely. When set, cache_ttl has no effect.

Set edns0_size to control the EDNS0 UDP buffer size (default 1232, set to 4096 for resolvers that support larger payloads).

Set protocol to "udp" (default), "tcp", or "tcp-tls" (DNS over TLS / DoT) to select the DNS transport. Timeout and EDNS0 size compose correctly with all protocols.

For tcp-tls, two optional fields control TLS:

  • tls_server_name β€” sets the SNI and expected cert hostname. Required when the server address is an IP but the cert is issued for a hostname. Use this with tls_skip_verify: false for full cert verification against a trusted CA.
  • tls_skip_verify β€” disables cert verification. Only for self-signed certs where no valid server name can be verified. Never use in production.

keep_alive_pool_size β€” enables persistent TCP/TLS connection pooling when set to a positive integer alongside protocol: tcp or protocol: tcp-tls. 0 (default) disables the pool; omitting the field entirely is equivalent to 0 and does not affect existing tcp/tcp-tls usage. Requires a server supporting RFC 7766 (tcp) or RFC 7858 (tcp-tls) β€” use with DoT providers (e.g. Cloudflare 1.1.1.1:853, Google 8.8.8.8:853) or modern local resolvers. The default Nawala ISP servers do not benefit.

⚑ 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 DNS-over-TLS (DoT) transport.
    nawala.WithProtocol("tcp-tls"),
    nawala.WithTLSServerName("dns.example.com"),

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

    // Digest-based cache keys: replace the plain key body with a hash so that
    // long or sensitive values are never stored verbatim in the cache backend.
    // The final key is always: "nawala_checker:<digest>".
    // SHA-256 (single-round) β€” standard, widely used.
    nawala.WithDigests(func(data string) string {
        sum := sha256.Sum256([]byte(data))
        return hex.EncodeToString(sum[:])
    }),
    // Or Bitcoin-style double-SHA-256: SHA256(SHA256(data)).
    // nawala.WithDigests(func(data string) string {
    //     first := sha256.Sum256([]byte(data))
    //     second := sha256.Sum256(first[:])
    //     return hex.EncodeToString(second[:])
    // }),
)

πŸ”§ 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)
WithDigests(fn) nil (off) Custom hash for cache keys; key format: nawala_checker:<digest> (pass nil to disable)
WithConcurrency(n) 100 Max concurrent DNS checks (semaphore size)
WithEDNS0Size(n) 1232 EDNS0 UDP buffer size (prevents fragmentation)
WithProtocol(s) "udp" DNS transport: "udp", "tcp", or "tcp-tls" (DoT)
WithTLSServerName(s) "" TLS SNI server name override (tcp-tls only)
WithTLSSkipVerify() false Skip TLS certificate verification (tcp-tls only)
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
Checker.Concurrency() β€” Returns the configured concurrency limit (semaphore size); useful for sizing output channel buffers to match in-flight capacity
WithKeepAlive(n) disabled Persistent TCP/TLS conn pool; n = max idle conns per server (≀0 β†’ min(concurrency,10)); requires RFC 7766 (tcp) or RFC 7858 (tcp-tls) server support β€” use with DoT providers or modern custom resolvers, not the default Nawala ISP servers; no-op for UDP

πŸ”Œ 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")

// Stream-check domains through a channel pipeline.
// Domains flow from In to Out as they complete β€” memory stays constant
// regardless of input size.
in := make(chan string)
out := make(chan nawala.Result, c.Concurrency())
go func() {
    for _, d := range domains { in <- d }
    close(in)
}()
err := c.CheckStream(ctx, nawala.Stream{In: in, Out: out})

// Read the configured concurrency (semaphore size).
// Useful for sizing buffers to match the checker's in-flight capacity.
n := c.Concurrency()

// 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")

// Release idle keep-alive connections when the checker is no longer needed.
c.Close()

βœ… 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  // DNS server to query: IP ("8.8.8.8"), IP:port ("8.8.8.8:5353"), hostname ("dns.example.com"), or hostname with port ("dns.example.com:5353"). Port defaults to 53 (or 853 for tcp-tls) when omitted.
    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()
}

πŸ”‘ Cache Key Format

All cache keys are namespaced with the prefix nawala_checker: to prevent collisions when multiple packages share the same backend (e.g., Redis). The default format is:

nawala_checker:<domain>:<server>:<keyword>:<qtype>

When WithDigests is configured, the raw components are hashed and the digest (which may include sub-namespaces for hierarchical caching) becomes the key body:

nawala_checker:<digest>

Where <digest> may include sub-namespaces like environment:<hash> or version:v1:<hash>.

Two ready-to-use hash functions:

import (
    "crypto/sha256"
    "encoding/hex"
)

// Standard SHA-256 (single-round)
func digestSHA256(data string) string {
    sum := sha256.Sum256([]byte(data))
    return hex.EncodeToString(sum[:])
}

// Bitcoin-style double-SHA-256: SHA256(SHA256(data))
func digestDoubleSHA256(data string) string {
    first := sha256.Sum256([]byte(data))
    second := sha256.Sum256(first[:])
    return hex.EncodeToString(second[:])
}

// Or with sub-namespace for hierarchical caching:
func digestWithSubNamespace(data string) string {
    sum := sha256.Sum256([]byte(data))
    return "environment:" + hex.EncodeToString(sum[:])
}

c := nawala.New(
    nawala.WithDigests(digestSHA256),          // or digestDoubleSHA256
    // nawala.WithDigests(digestWithSubNamespace), // for hierarchical keys
)

Use WithDigests when:

  • The cache backend enforces a maximum key length
  • Internal server addresses must not appear in keys in plain text
  • A consistent, fixed-width key format (64-char hex) is required
  • Cache backends require hierarchical key structures or sub-namespacing

πŸ“ 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
streaming Stream domains through a channel pipeline for constant-memory operation
pooling Connection pooling for TCP/TLS to eliminate handshake overhead

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
β”œβ”€β”€ cmd/
β”‚   └── nawala/         # CLI entry point
β”œβ”€β”€ examples/           # Runnable usage examples (basic, custom, status, hotreload, streaming, pooling)
β”œβ”€β”€ skills/             # AI agent skill definitions (for opencode, openclaw, crush, etc.)
β”œβ”€β”€ internal/
β”‚   └── cli/            # CLI package (commands, config, output)
β”œβ”€β”€ 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

# Build the CLI binary.
make build

πŸ—ΊοΈ 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.
  • Implement MCP (Model Context Protocol) support with both an SDK version (helpers for agentic AI frameworks/tools like opencode, openclaw, crush, and others) and a CLI/server version (bundled in this repository) for integrating nawala-checker directly with LLMs and AI agents via the standard MCP protocol.
  • Implement a pure JSON-RPC 2.0 server version (bundled in this repository) for language-agnostic integration over stdio or TCP, similar in spirit to how MCP works but using the standard JSON-RPC wire protocol.

πŸ“„ 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

Code of conduct

Contributing

Stars

10 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages