自己实现一个DNS服务
本文最后更新于 2026年3月9日
有时,我们所在单位的电脑只允许上内网,外网被断掉了,如果想要同时上内外网,我们可以通过修改路由表,然后双网卡一机两网的方式来实现分流上网,例如网线连公司内网,用WiFi连接自己的手机热点,或者额外购买一个USB网卡插入电脑,同时连接公司的AP和自己手机热点。
但是这样会衍生出一个问题,有些公司的内部系统例如OA系统等,也是通过域名而不是难以记忆的IP地址来访问的,这些内部系统的域名不是注册商注册的,更不在公共DNS上,而是公司内网上使用的内网域名,使用公司自建的内网DNS服务器才能解析,解析出通常是一个本地局域网地址,在公网无法解析和访问,当接入公司内网,企业路由器会通过DHCP下发内网DNS给网卡,现在同时上内外网时,外网网卡也会获得运营商下发的外网DNS地址,操作系统会按照跃点数只选择某个网卡上获得的的DNS用作DNS解析,如果默认了内网网卡优先,且内网DNS只解析公司内网域名,同样会导致外网无法访问,如果内网DNS能解析外部域名,同样存在利用DNS屏蔽某些网站或服务(例如影视剧,游戏,向日葵远控等)甚至后台偷偷记录DNS解析记录的可能,因此为了保险起见,我们可以自己用代码实现一个DNS代理服务器来进行代理和分流,根据特定后缀等特征判断出内网域名,交给内网DNS解析,对于外网域名则直接选择一些公共DNS来解析(例如谷歌,阿里,114的DNS服务)
这里采用Java实现一个多线程的DNS代理服务器,对于内网域名直接通过内网DNS的UDP:53进行解析,对于外网域名则以加密的DOH(DNS Over Https)方式通过阿里云DNS进行解析,并解析DNS服务器返回的报文并打印日志。需要依赖dnsjava这个类库的支持,程序启动后,只需要将网卡DNS服务器地址和备用地址修改为127.0.0.1和127.0.0.2即可实现DNS的分流。
<dependencies>
<!-- DNS 处理库 -->
<dependency>
<groupId>dnsjava</groupId>
<artifactId>dnsjava</artifactId>
<version>3.6.0</version>
</dependency>
<!-- HTTP 客户端(用于DoH请求) -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
</dependencies>package com.changelzj.dns;
import org.apache.hc.core5.http.ContentType;
import org.xbill.DNS.*;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
public class LoggedDnsServer {
/**
* 需要内网DNS才能解析的内网域名
*/
private static final String[] INTERNAL_DOMAINS = {"p****c.com", "s******c.com"};
/**
* 内网NDS服务器IP地址
*/
private static final String INTERNAL_DNS = "10.249.35.11";
private static final String DOH_URL = "https://223.5.5.5/dns-query";
private static final ExecutorService executor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2,
Runtime.getRuntime().availableProcessors() * 2,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(200),
new ThreadPoolExecutor.CallerRunsPolicy()
);
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(53);
System.out.println("Multi-threaded DNS Server with Logging started on port 53");
byte[] buffer = new byte[512];
while (true) {
DatagramPacket requestPacket = new DatagramPacket(buffer, buffer.length);
socket.receive(requestPacket);
byte[] requestData = new byte[requestPacket.getLength()];
System.arraycopy(requestPacket.getData(), 0, requestData, 0, requestPacket.getLength());
executor.submit(() -> {
Instant start = Instant.now();
String domain = "";
String method = "";
boolean success = false;
String ip = "";
try {
Message query = new Message(requestData);
domain = query.getQuestion().getName().toString(true).toLowerCase();
byte[] responseData;
if (isInternalDomain(domain)) {
method = "Internal DNS (" + INTERNAL_DNS + ")";
responseData = forwardToUdpDns(query, INTERNAL_DNS);
} else {
method = "Ali DNS DoH (" + DOH_URL + ")";
responseData = forwardToDoh(query);
}
success = true;
ip = parseDnsResponse(responseData).toString();
DatagramPacket responsePacket = new DatagramPacket(
responseData,
responseData.length,
requestPacket.getAddress(),
requestPacket.getPort()
);
socket.send(responsePacket);
} catch (Exception e) {
System.err.println("[ERROR] " + e.getMessage());
} finally {
long ms = Duration.between(start, Instant.now()).toMillis();
System.out.printf(
"[%s] %s -> %s | %s | %s | %dms | %s %n",
requestPacket.getAddress().getHostAddress(),
domain,
method,
success ? "OK" : "FAIL",
ip,
ms,
Thread.currentThread().getName()
);
}
});
}
}
private static boolean isInternalDomain(String domain) {
for (String suffix : INTERNAL_DOMAINS) {
if (domain.endsWith(suffix)) {
return true;
}
}
return false;
}
private static byte[] forwardToUdpDns(Message query, String dnsServer) throws IOException {
SimpleResolver resolver = new SimpleResolver(dnsServer);
resolver.setTCP(false);
resolver.setTimeout(3);
Message response = resolver.send(query);
return response.toWire();
}
private static byte[] forwardToDoh(Message query) throws IOException {
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpPost post = new HttpPost(DOH_URL);
post.setHeader("Content-Type", "application/dns-message");
post.setEntity(new ByteArrayEntity(query.toWire(), ContentType.create("application/dns-message")));
return client.execute(post, httpResponse -> {
try (java.io.InputStream in = httpResponse.getEntity().getContent();
java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream()) {
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) {
bos.write(buf, 0, len);
}
return bos.toByteArray();
}
});
}
}
public static List<String> parseDnsResponse(byte[] msg) throws Exception {
List<String> result = new ArrayList<>();
int pos = 0;
// 头部 12 字节
pos += 4; // ID + Flags
int qdCount = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;
int anCount = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;
int nsCount = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;
int arCount = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;
// 跳过 Question 区
for (int i = 0; i < qdCount; i++) {
// 读 QNAME(支持压缩指针)
pos = readName(msg, pos, null);
pos += 4; // QTYPE + QCLASS
}
int rrCount = anCount + nsCount + arCount;
for (int i = 0; i < rrCount; i++) {
pos = readName(msg, pos, null);
int type = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;
pos += 2; // CLASS
pos += 4; // TTL
int rdlen = ((msg[pos] & 0xFF) << 8) | (msg[pos + 1] & 0xFF); pos += 2;
if (type == 1 && rdlen == 4) { // A
byte[] addr = Arrays.copyOfRange(msg, pos, pos + 4);
result.add(InetAddress.getByAddress(addr).getHostAddress());
} else if (type == 28 && rdlen == 16) { // AAAA
byte[] addr = Arrays.copyOfRange(msg, pos, pos + 16);
result.add(InetAddress.getByAddress(addr).getHostAddress());
}
pos += rdlen;
}
return result;
}
// 工具:读取域名(含压缩指针),返回新的 pos
private static int readName(byte[] msg, int pos, StringBuilder out) {
int jumpedPos = -1;
while (true) {
int len = msg[pos] & 0xFF;
if ((len & 0xC0) == 0xC0) { // 压缩
int ptr = ((len & 0x3F) << 8) | (msg[pos + 1] & 0xFF);
if (jumpedPos == -1) jumpedPos = pos + 2;
pos = ptr;
continue;
}
pos++;
if (len == 0) break;
if (out != null) {
if (out.length() > 0) out.append('.');
out.append(new String(msg, pos, len, StandardCharsets.ISO_8859_1));
}
pos += len;
}
return jumpedPos != -1 ? jumpedPos : pos;
}
}
同样的功能,python也可以实现
import socket
import threading
from dnslib import DNSRecord, QTYPE, RR
# --- 核心配置 ---
LISTEN_IP = '0.0.0.0' # 监听所有网卡
LISTEN_PORT = 53
INTERNAL_DNS = '10.249.35.11'
EXTERNAL_DNS = '114.114.114.114'
TARGET_DOMAINS = ['pc**.com', 'si*.com']
def should_use_internal(domain_name):
domain = domain_name.rstrip('.').lower()
for target in TARGET_DOMAINS:
if domain == target or domain.endswith('.' + target):
return True
return False
def handle_request(data, addr, server_sock):
try:
request = DNSRecord.parse(data)
query_domain = str(request.q.qname)
query_type = QTYPE[request.q.qtype]
# --- 策略 1: 屏蔽 IPv6 查询 (AAAA) ---
if query_type == 'AAAA':
reply = request.reply()
# 直接返回空结果(无错误,但没记录),强制客户端尝试 IPv4
server_sock.sendto(reply.pack(), addr)
print(f"[屏蔽 IPv6] {query_domain}")
return
# --- 策略 2: 分流判断 ---
upstream = INTERNAL_DNS if should_use_internal(query_domain) else EXTERNAL_DNS
# 转发请求给上游
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as proxy_sock:
proxy_sock.settimeout(2.5)
proxy_sock.sendto(data, (upstream, 53))
try:
raw_reply, _ = proxy_sock.recvfrom(4096)
# --- 策略 3: 二次过滤返回报文中的 IPv6 记录 ---
reply_record = DNSRecord.parse(raw_reply)
# 只保留 A 记录 (IPv4) 和 CNAME 记录
reply_record.rr = [r for r in reply_record.rr if QTYPE[r.rtype] in ['A', 'CNAME']]
server_sock.sendto(reply_record.pack(), addr)
print(f"[IPv4 转发] {query_domain} -> {upstream}")
except socket.timeout:
print(f"[超时] 上游 {upstream} 未响应 {query_domain}")
except Exception as e:
print(f"[错误] {e}")
def start_proxy():
# Windows 环境下建议先尝试绑定 127.0.0.1
server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
server_sock.bind((LISTEN_IP, LISTEN_PORT))
print(f"DNS 代理已就绪...")
print(f"监听地址: {LISTEN_IP}:{LISTEN_PORT}")
print(f"内网解析: {TARGET_DOMAINS} -> {INTERNAL_DNS}")
print(f"外网解析: 其他 -> {EXTERNAL_DNS}")
except PermissionError:
print("!!! 权限不足: 请右键使用'管理员身份'运行 CMD/PyCharm !!!")
return
except Exception as e:
print(f"启动失败: {e}")
return
while True:
try:
data, addr = server_sock.recvfrom(512)
threading.Thread(target=handle_request, args=(data, addr, server_sock), daemon=True).start()
except:
pass
if __name__ == '__main__':
start_proxy()"如果文章对您有帮助,可以请作者喝杯咖啡吗?"
微信支付
支付宝