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.
- 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
Cacheinterface - 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
WithDigestsoption to replace the plain key body with any hash (e.g. hex SHA-256 or Bitcoin-style double-SHA-256), producingnawala_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.Ismatching (see Errors) - Connection pooling β reuse established TCP/TLS connections via
WithKeepAliveto eliminate handshake overhead - Runtime hot-reload β safely add, replace, or remove DNS servers concurrently without restarting
- Agentic AI skills β ready-to-use
SKILL.mddefinitions for seamless integration with opencode, openclaw, crush and other LLM agents (see skills/)
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)β raisento 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.
go get github.com/H0llyW00dzZ/nawala-checkerRequires Go 1.25.6 or later.
Install the nawala command-line tool:
go install github.com/H0llyW00dzZ/nawala-checker/cmd/nawala@latestUsage:
# 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 --versionNote
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 withtls_skip_verify: falsefor 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.
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)
}
}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[:])
// }),
)| 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 |
// 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()// Validate a domain name before checking.
ok := nawala.IsValidDomain("example.com") // true
ok = nawala.IsValidDomain("invalid") // false (single label, no TLD)// 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.
}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)
)Implement the Cache interface to use a custom cache backend:
type Cache interface {
Get(key string) (Result, bool)
Set(key string, val Result)
Flush()
}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
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/basicThe 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.
Indonesian ISP DNS filters use two distinct blocking mechanisms:
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 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",
},
})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.
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)
Tests must be run from a cloned repository:
git clone https://github.com/H0llyW00dzZ/nawala-checker.git
cd nawala-checkerThen 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- Upgrade
github.com/miekg/dnsto 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.
BSD 3-Clause License β Copyright (c) 2026, H0llyW00dzZ