自己实现一个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.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;
    }

}

同样的功能,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()

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

微信二维码

微信支付

支付宝二维码

支付宝


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