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
- 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 - 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)
go get github.com/H0llyW00dzZ/nawala-checkerRequires Go 1.25.6 or later.
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 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),
)| 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 |
// 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")// 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 // 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.
}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()
}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/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
├── examples/ # Runnable usage examples (basic, custom, status)
├── 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- 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.
BSD 3-Clause License — Copyright (c) 2026, H0llyW00dzZ