使用Java实现一个DNS服务
本文最后更新于 2025年9月1日
有时,我们所在单位的电脑只允许上内网,外网被断掉了,如果想要同时上内外网,我们可以通过修改路由表,然后双网卡一机两网的方式来实现分流上网,例如网线连公司内网,用WiFi连接自己的手机热点,或者额外购买一个USB网卡插入电脑,同时连接公司的AP和自己手机热点。
但是这样会衍生出一个问题,有些公司的内网网站例如OA系统等,也是通过域名而不是难以记忆的IP地址来访问的,这种公司内网域名是通过公司自建的内网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;
}
}
"如果文章对您有帮助,可以请作者喝杯咖啡吗?"

微信支付

支付宝