A DNS tunnel implementation in Go with two modes: Message Mode (simple send/receive) and TUN Network Layer Mode (virtual NIC for any IP traffic).
English | 中文
DNS Tunnel is a Go implementation of a secure communication channel over DNS protocol. It uses DNS TXT queries to transmit data, bypassing traditional network restrictions. The project supports two operating modes:
- Message Mode: Send/receive encrypted text messages via DNS TXT queries (original API)
- TUN Mode: Create a virtual network interface (TUN) to tunnel any IP traffic (HTTP, SSH, DNS, etc.) over DNS
- Dual mode: Message mode for simple text communication, TUN mode for full IP tunneling
- Encryption (optional): ChaCha20-Poly1305 for secure communication when needed
- DNS protocol: Transmits data through DNS TXT queries, bypassing firewalls
- Configurable: Customizable DNS server, domain length limits, and session ID length
- Asynchronous: Server uses goroutines for concurrent request handling
- Lightweight: Minimal dependencies (miekg/dns, songgao/water, golang.org/x/crypto)
Client → [encrypt → base32 → DNS TXT query] → Server → [decrypt → channel]
Client App → [Client TUN 10.0.0.2/24] → DNS queries → [Server DNS :53] → [Server TUN 10.0.0.1/24] → Target Network
↑ DNS responses ↓
└─────────────────── Bidirectional IP tunnel ────────────────────┘
git clone https://github.com/mkaaad/dns-tunnel.git
cd dns-tunnel
go build ./...Simple text message tunnel (original API).
Server:
package main
import (
"fmt"
"github.com/mkaaad/dns-tunnel/server"
)
func main() {
key := []byte("0123456789abcdef0123456789abcdef") // 32 bytes, or nil for no encryption
msgChan := server.ListenAndServer("your-domain.com", key)
go func() {
for msg := range msgChan {
fmt.Println("Received:", msg)
}
}()
select {}
}go run ./cmd/server your-domain.com "0123456789abcdef0123456789abcdef"Client:
package main
import (
"fmt"
"github.com/mkaaad/dns-tunnel/client"
)
func main() {
key := []byte("0123456789abcdef0123456789abcdef")
c := client.NewClient("your-domain.com", key)
err := c.Do("Hello, DNS tunnel!")
if err != nil {
panic(err)
}
fmt.Println("Message sent!")
}go run ./cmd/client your-domain.com "0123456789abcdef0123456789abcdef" "Hello"Create a virtual network interface to tunnel any IP traffic. Requires root privileges.
Direct connection (server IP reachable):
Server:
sudo go run ./cmd/tunnel-server -domain example.com -tun 10.0.0.1/24Client:
sudo go run ./cmd/tunnel-client \
-domain example.com \
-tun 10.0.0.2/24 \
-gateway 10.0.0.1 \
-dns <server-public-ip>:53 \
-route 0.0.0.0/0Public deployment (client only reaches internal DNS):
This is the classic DNS tunneling scenario. Client DNS queries reach your server through standard recursive resolution:
Client → Internal DNS → Root DNS → .com TLD → Authoritative NS → Your Tunnel Server :53
Requirements:
- Buy a domain (e.g.,
tunnel.com), set NS record to your server IP - Open UDP 53 on your server
- Client omits
-dnsto use the system resolver (internal DNS):
# Client in restricted network — no -dns flag, queries go through internal DNS
sudo go run ./cmd/tunnel-client \
-domain tunnel.com \
-tun 10.0.0.2/24 \
-gateway 10.0.0.1 \
-route 0.0.0.0/0The internal DNS will recursively resolve *.tunnel.com and eventually reach your server. No direct connectivity needed.
- Client creates TUN device
tun0with IP10.0.0.2/24 - Automatically adds an exception route so DNS queries to
1.2.3.4go through the original network interface (not TUN) — avoids routing loops - Adds the route
0.0.0.0/0 via 10.0.0.1 dev tun0— all other traffic goes through the tunnel - Client reads IP packets from TUN, fragments them, base32-encodes, and sends as DNS TXT queries to
1.2.3.4:53 - Server reassembles, writes to its TUN, reads return traffic, and sends back in DNS responses
- Result: transparent IP tunnel — any application works without modification
Routing safety: When -dns is specified, the client automatically adds a /32 exception route for the DNS server IP via the original gateway, preventing DNS queries from looping through the TUN interface. When -dns is omitted (system resolver), DNS queries use the physical network directly — no loop possible.
Programmatic API (with optional encryption):
package main
import (
"github.com/mkaaad/dns-tunnel/tunnel"
)
func main() {
// Without encryption (faster)
srv, _ := tunnel.NewServer("example.com", nil, "10.0.0.1/24")
srv.ListenAndServe()
// With encryption (ChaCha20-Poly1305)
key := []byte("0123456789abcdef0123456789abcdef")
srv, _ = tunnel.NewServer("example.com", key, "10.0.0.1/24")
srv.ListenAndServe()
}package main
import (
"github.com/mkaaad/dns-tunnel/tunnel"
)
func main() {
// Without encryption (faster)
cli, _ := tunnel.NewClient("example.com", nil, "10.0.0.2/24", "1.2.3.4:53")
cli.Run()
// With encryption
key := []byte("0123456789abcdef0123456789abcdef")
cli, _ = tunnel.NewClient("example.com", key, "10.0.0.2/24", "1.2.3.4:53")
cli.Run()
}config := &client.ClientConfig{
MaxLength: 253, // Maximum DNS record length (1-253)
MaxLabelLength: 63, // Maximum label length (1-63)
BaseDomain: "example.com",
Key: key, // 32-byte key (nil = no encryption)
MessageIDLength: 4, // Length of message IDs
DNSServer: "8.8.8.8:53", // Custom DNS server (optional)
}
c := client.NewClientWithConfig(config).
├── client/ # Message mode: client implementation
│ ├── client.go # Client struct, Do() method, config
│ └── utils.go # Encryption, splitting, random strings
├── server/ # Message mode: DNS server
│ └── server.go # DNS handler, ListenAndServer, decryption
├── tun/ # TUN device management (multi-platform)
│ ├── interface.go # Core Interface type wrapping *os.File
│ ├── tun.go # Linux: NewTUN() using water + ip (build: linux,!android)
│ ├── tun_darwin.go # macOS: ifconfig-based TUN config (build: darwin)
│ ├── tun_windows.go # Windows: netsh-based TUN config (build: windows)
│ └── tun_android.go # Android: NewTUNFromFD() for VpnService fds
├── tunnel/ # TUN network layer tunnel
│ ├── client.go # Tunnel client: TUN ↔ DNS queries
│ ├── server.go # Tunnel server: DNS queries ↔ TUN
│ ├── crypto.go # ChaCha20-Poly1305 encrypt/decrypt (exported)
│ └── transport.go # Protocol: fragmentation, base32, query parsing
├── cmd/
│ ├── client/ # Message mode client executable
│ ├── server/ # Message mode server executable
│ ├── tunnel-client/ # TUN mode client executable
│ └── tunnel-server/ # TUN mode server executable
├── native/ # Cross-platform shared library (.so/.dylib/.dll)
│ ├── mobile.go # CGo exports: DnsTunnel_StartClientWithFD (Android)
│ ├── tun_client.go # CGo exports: DnsTunnel_StartClient (Linux/macOS/Windows)
│ ├── build_linux.sh # Build script for libdns-tunnel.so
│ ├── build_macos.sh # Build script for libdns-tunnel.dylib
│ └── build_windows.sh# Build script for dns-tunnel.dll
├── go.mod
├── go.sum
└── README.md
- Go 1.24.4+ (as specified in go.mod)
- Root privileges for server (binds to port 53) and tunnel mode (creates TUN devices)
- TUN module: Linux kernel module (
tun) — usually loaded by default - iproute2: For TUN IP configuration (
ip addr,ip link,ip route)
# Format code
go fmt ./...
# Check for issues
go vet ./...
# Build all packages
go build ./...
# Run tests (none currently implemented)
go test ./...Each DNS transaction carries IP packet fragments in both directions:
Client → Server (DNS query):
<base32_data>.<flags>.<session_id>.<base_domain>
Server → Client (DNS response):
TXT: ["ok", base32_return_data_chunks...]
- Flags:
f= fragment (more to come),l= last fragment,p= poll (no data) - Fragmentation: IP packets split into 145-byte chunks (before base32 encoding) to fit DNS limits
- Polling: Client sends keepalive queries every 200ms to fetch return traffic
- Return data: Response carries full IP packet (base32 encoded, split across TXT strings)
Build the tunnel client as a shared library (.so/.dylib/.dll) for use from other languages (C, Kotlin, Swift, Dart, etc.).
Three exported functions (defined in native/mobile.go and native/tun_client.go with //export directives):
// Start the DNS tunnel (Linux/macOS/Windows). Go creates the TUN device internally.
int DnsTunnel_StartClient(char* domain, char* dnsServer, char* tunAddr);
// Start the DNS tunnel with an externally created TUN fd (Android).
int DnsTunnel_StartClientWithFD(int tunFd, char* domain, char* dnsServer, char* tunAddr);
// Stop the tunnel.
void DnsTunnel_StopClient(void);cd /root/code/dns-tunnel
CGO_ENABLED=1 go build -buildmode=c-shared -o libdns-tunnel.so ./nativeOutput: libdns-tunnel.so + libdns-tunnel.h.
CGO_ENABLED=1 GOOS=darwin go build -buildmode=c-shared -o libdns-tunnel.dylib ./nativeOutput: libdns-tunnel.dylib + libdns-tunnel.h.
Requires MinGW-w64 cross-compiler.
CGO_ENABLED=1 GOOS=windows CC=x86_64-w64-mingw32-gcc \
go build -buildmode=c-shared -o dns-tunnel.dll ./nativeOutput: dns-tunnel.dll + dns-tunnel.h.
Prerequisites: Android NDK (NDK environment variable).
export NDK=/path/to/android-ndk
./native/build_android.shOutput: libdns-tunnel.so (arm64-v8a) + libdns-tunnel.h.
class TunnelVpnService : VpnService() {
private var tunnelFd: Int? = null
init { System.loadLibrary("dns-tunnel") }
private external fun DnsTunnel_StartClientWithFD(
fd: Int, domain: String, dnsServer: String, tunAddr: String): Int
private external fun DnsTunnel_StopClient()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val builder = Builder()
builder.addAddress("10.0.0.2", 24)
builder.addRoute("0.0.0.0", 0)
builder.setMtu(1500)
builder.addRoute("1.2.3.4", 32) // DNS server exception
val vpn = builder.establish() ?: return START_STICKY
tunnelFd = vpn.detachFd()
vpn.close()
DnsTunnel_StartClientWithFD(tunnelFd!!, "example.com",
"1.2.3.4:53", "10.0.0.2/24")
return START_STICKY
}
override fun onDestroy() {
DnsTunnel_StopClient()
tunnelFd?.let { try { ParcelFileDescriptor.fromFd(it).close() } catch(_: Exception) {} }
super.onDestroy()
}
}Desktop (Linux/macOS/Windows):
App (C/Kotlin/Swift/Dart) → Go .so (native/tun_client.go) → [water TUN] → tunnel.Client
DNS queries ↓
└─────────────────── DNS tunnel ──────→ Server
Android:
Android App → [VpnService: TUN fd] → Go .so (native/mobile.go) → tunnel.Client
↑ DNS queries ↓
└─────────────────── DNS tunnel ────────────────→ Server
| Metric | With Encryption | Without Encryption |
|---|---|---|
| Payload per fragment | 105 bytes | 145 bytes |
| Queries per 1500B IP packet | ~15 | ~11 |
| Relative throughput | baseline | +38% |
- Port 53: Server requires root privileges to bind to port 53
- DNS limitations: Maximum label length 63 chars, total length 253 chars
- Throughput: Limited by DNS query latency (typically 50-200ms RTT)
- TUN device creation: Linux uses
ipcommands, macOS usesifconfig, Windows usesnetsh - Single-threaded poll: Return traffic polling at 200ms intervals adds latency
- Encryption is optional — the network layer doesn't require it (higher layers like TLS/SSH provide encryption)
- When using encryption, use strong, randomly generated 32-byte keys
- The server listens on all interfaces by default (0.0.0.0:53)
- Consider firewall rules and DNS server configuration
- Encryption uses ChaCha20-Poly1305, which is considered secure
- Fork the repository
- Create a feature branch
- Make changes following existing code patterns
- Run
go fmtbefore committing - Submit a pull request
DNS隧道是一个用Go语言实现的DNS隧道工具,支持两种模式:消息模式(简单收发文本消息)和 TUN网络层模式(虚拟网卡,转发任意IP流量)。
- 双模式:消息模式用于简单文本通信,TUN模式用于完整的IP隧道
- 加密可选:ChaCha20-Poly1305加密,不需要可不加
- DNS协议:通过DNS TXT查询传输数据,绕过防火墙
- 可配置:可自定义DNS服务器、域名长度限制和会话ID长度
- 异步处理:服务器使用goroutine进行并发请求处理
- 轻量级:依赖极少(miekg/dns, songgao/water, golang.org/x/crypto)
客户端 → [加密 → base32 → DNS TXT查询] → 服务端 → [解密 → 通道]
应用 → [Client TUN 10.0.0.2] → DNS查询 → [服务端DNS :53] → [Server TUN 10.0.0.1] → 目标网络
↑ DNS响应 ↓
└─────────────────── 双向IP隧道 ─────────────────────────┘
服务端(公网IP 1.2.3.4):
sudo go run ./cmd/tunnel-server \
-domain example.com \
-tun 10.0.0.1/24客户端:
sudo go run ./cmd/tunnel-client \
-domain example.com \
-tun 10.0.0.2/24 \
-gateway 10.0.0.1 \
-dns 1.2.3.4:53 \
-route 0.0.0.0/0工作流程:
- 客户端创建TUN设备,IP
10.0.0.2/24 - 自动添加DNS服务器IP的异常路由(
1.2.3.4/32 via 原网关),避免环路 - 添加隧道路由(
0.0.0.0/0 via 10.0.0.1 dev tun0),其余流量走隧道 - 客户端从TUN读取IP包 → 分片 → base32编码 → DNS TXT查询发送到
1.2.3.4:53 - 服务端重组 → 写入TUN → 读取回程流量 → DNS响应返回给客户端
- 结果:透明IP隧道,任何应用无需修改即可使用
编程接口(可选加密):
// 不加密(更快)
cli, _ := tunnel.NewClient("example.com", nil, "10.0.0.2/24", "1.2.3.4:53")
cli.Run()
// 加密
key := []byte("0123456789abcdef0123456789abcdef")
cli, _ = tunnel.NewClient("example.com", key, "10.0.0.2/24", "1.2.3.4:53")
cli.Run()// 服务端
msgChan := server.ListenAndServer("your-domain.com", key)
// 客户端
c := client.NewClient("your-domain.com", key)
c.Do("Hello, DNS tunnel!").
├── client/ # 消息模式:客户端实现
├── server/ # 消息模式:DNS服务端
├── tun/ # TUN设备管理(多平台)
│ ├── interface.go # 核心 Interface 类型,封装 *os.File
│ ├── tun.go # Linux: water + ip 命令
│ ├── tun_darwin.go # macOS: ifconfig 配置 TUN
│ ├── tun_windows.go # Windows: netsh 配置 TUN
│ └── tun_android.go # Android: fd-based TUN
├── tunnel/ # TUN网络层隧道
│ ├── client.go # 隧道客户端:TUN ↔ DNS查询
│ ├── server.go # 隧道服务端:DNS查询 ↔ TUN
│ ├── crypto.go # 加解密(导出Encrypt/Decrypt)
│ └── transport.go # 协议:分片、base32、查询解析
├── cmd/
│ ├── client/ # 消息模式客户端入口
│ ├── server/ # 消息模式服务端入口
│ ├── tunnel-client/ # TUN模式客户端入口
│ └── tunnel-server/ # TUN模式服务端入口
├── native/ # 跨平台共享库
│ ├── mobile.go # CGo 导出: DnsTunnel_StartClientWithFD (Android)
│ ├── tun_client.go # CGo 导出: DnsTunnel_StartClient (桌面)
│ ├── build_linux.sh
│ ├── build_macos.sh
│ └── build_windows.sh
├── go.mod
└── go.sum
git clone https://github.com/mkaaad/dns-tunnel.git
cd dns-tunnel
go build ./...简单文本消息隧道(原始API)。
服务端:
package main
import (
"fmt"
"github.com/mkaaad/dns-tunnel/server"
)
func main() {
key := []byte("0123456789abcdef0123456789abcdef") // 32字节,或nil不加密
msgChan := server.ListenAndServer("your-domain.com", key)
go func() {
for msg := range msgChan {
fmt.Println("Received:", msg)
}
}()
select {}
}sudo go run ./cmd/server your-domain.com "0123456789abcdef0123456789abcdef"客户端:
package main
import (
"fmt"
"github.com/mkaaad/dns-tunnel/client"
)
func main() {
key := []byte("0123456789abcdef0123456789abcdef")
c := client.NewClient("your-domain.com", key)
err := c.Do("Hello, DNS tunnel!")
if err != nil {
panic(err)
}
fmt.Println("Message sent!")
}go run ./cmd/client your-domain.com "0123456789abcdef0123456789abcdef" "你好"config := &client.ClientConfig{
MaxLength: 253, // DNS记录最大长度 (1-253)
MaxLabelLength: 63, // 标签最大长度 (1-63)
BaseDomain: "example.com",
Key: key, // 32字节密钥 (nil = 不加密)
MessageIDLength: 4, // 消息ID长度
DNSServer: "8.8.8.8:53", // 自定义DNS服务器 (可选)
}
c := client.NewClientWithConfig(config)# 格式化代码
go fmt ./...
# 静态检查
go vet ./...
# 构建所有包
go build ./...
# 运行测试(目前未实现)
go test ./...每个DNS事务双向传输IP包分片:
客户端 → 服务端(DNS查询):
<base32数据>.<标志>.<会话ID>.<基础域名>
服务端 → 客户端(DNS响应):
TXT: ["ok", base32回传数据块...]
- 标志:
f= 还有分片,l= 最后分片,p= 轮询(无数据) - 分片:IP包分割为145字节块(base32编码前),以适应DNS限制
- 轮询:客户端每200ms发送保活查询以获取回程流量
- 回传数据:响应携带完整IP包(base32编码,跨TXT字符串分割)
将隧道客户端编译为共享库(.so/.dylib/.dll),供其他语言(C、Kotlin、Swift、Dart 等)调用。
// 启动 DNS 隧道(Linux/macOS/Windows)。Go 内部创建 TUN 设备。
int DnsTunnel_StartClient(char* domain, char* dnsServer, char* tunAddr);
// 启动 DNS 隧道(Android)。传入 VpnService 的 TUN fd。
int DnsTunnel_StartClientWithFD(int tunFd, char* domain, char* dnsServer, char* tunAddr);
// 停止隧道
void DnsTunnel_StopClient(void);CGO_ENABLED=1 go build -buildmode=c-shared -o libdns-tunnel.so ./native输出:libdns-tunnel.so + libdns-tunnel.h。
CGO_ENABLED=1 GOOS=darwin go build -buildmode=c-shared -o libdns-tunnel.dylib ./native输出:libdns-tunnel.dylib + libdns-tunnel.h。
需要 MinGW-w64 交叉编译器。
CGO_ENABLED=1 GOOS=windows CC=x86_64-w64-mingw32-gcc \
go build -buildmode=c-shared -o dns-tunnel.dll ./native输出:dns-tunnel.dll + dns-tunnel.h。
需要 Android NDK(NDK 环境变量)。
export NDK=/path/to/android-ndk
./native/build_android.sh输出:libdns-tunnel.so(arm64-v8a)+ libdns-tunnel.h。
class TunnelVpnService : VpnService() {
private var tunnelFd: Int? = null
init { System.loadLibrary("dns-tunnel") }
private external fun DnsTunnel_StartClientWithFD(
fd: Int, domain: String, dnsServer: String, tunAddr: String): Int
private external fun DnsTunnel_StopClient()
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val builder = Builder()
builder.addAddress("10.0.0.2", 24)
builder.addRoute("0.0.0.0", 0)
builder.setMtu(1500)
builder.addRoute("1.2.3.4", 32) // DNS 服务器异常路由
val vpn = builder.establish() ?: return START_STICKY
tunnelFd = vpn.detachFd()
vpn.close()
DnsTunnel_StartClientWithFD(tunnelFd!!, "example.com",
"1.2.3.4:53", "10.0.0.2/24")
return START_STICKY
}
override fun onDestroy() {
DnsTunnel_StopClient()
tunnelFd?.let { try { ParcelFileDescriptor.fromFd(it).close() } catch(_: Exception) {} }
super.onDestroy()
}
}桌面端 (Linux/macOS/Windows):
应用 (C/Kotlin/Swift/Dart) → Go .so (native/tun_client.go) → [water TUN] → tunnel.Client
DNS 查询 ↓
└─────────────────── DNS 隧道 ──────→ 服务端
Android:
Android App → [VpnService: TUN fd] → Go .so (native/mobile.go) → tunnel.Client
↑ DNS 查询 ↓
└─────────────────── DNS 隧道 ────────────────→ 服务端
| 指标 | 加密 | 不加密 |
|---|---|---|
| 每分片负载 | 105 字节 | 145 字节 |
| 每1500B IP包查询数 | ~15 | ~11 |
| 相对吞吐量 | 基准 | +38% |
- 53端口:服务端需要root权限绑定53端口
- DNS限制:标签最长63字符,总长度最长253字符
- 吞吐量:受DNS查询延迟限制(通常50-200ms RTT)
- TUN设备:Linux 使用
ip,macOS 使用ifconfig,Windows 使用netsh - 单线程轮询:200ms间隔的回传流量轮询增加延迟
- 加密是可选的——网络层不需要(TLS/SSH等上层协议自带加密)
- 使用加密时,使用强随机生成的32字节密钥
- 服务端默认监听所有接口(0.0.0.0:53)
- 建议配置防火墙规则
- 加密使用ChaCha20-Poly1305,被认为是安全的
- Fork仓库
- 创建功能分支
- 按照现有代码风格修改
- 提交前运行
go fmt - 提交Pull Request
- Go 1.24.4+
- Root权限:服务端绑定53端口,TUN模式创建虚拟网卡
- TUN模块:Linux内核模块(通常默认加载)
- iproute2:TUN的IP配置