使用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.1127.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;
    }

}

"如果文章对您有帮助,可以请作者喝杯咖啡吗?"

微信二维码

微信支付

支付宝二维码

支付宝


使用Java实现一个DNS服务
https://blog.liuzijian.com/post/essay/2025/08/14/java-dns.html
作者
Liu Zijian
发布于
2025年8月14日
更新于
2025年9月1日
许可协议