惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

The GitHub Blog
The GitHub Blog
阮一峰的网络日志
阮一峰的网络日志
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Apple Machine Learning Research
Apple Machine Learning Research
小众软件
小众软件
博客园 - 司徒正美
Last Week in AI
Last Week in AI
爱范儿
爱范儿
罗磊的独立博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
Jina AI
Jina AI
人人都是产品经理
人人都是产品经理
量子位
V
V2EX
博客园 - 叶小钗
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog

Liu Zijian's Blog | 一个技术博客

使用Certbot自动续签HTTPS证书 使用Filebeat采集Nginx日志到ES Python的协程 Python中的异常 Python中的类和对象 Python的函数 Python的数据结构,推导式、迭代器和生成器 Spring AI集成多模态模型 LangChain4j多模态 LangChain Tools工具使用 Python中的模块和包 Python全局环境和虚拟环境(venv) LangChain Prompt提示词工程 LangChain4j Tools工具使用 基于Dify搭建AI智能体应用 LangChain4j RAG检索增强生成 Spring AI实现MCP Server Spring AI集成MCP Client LangChain4j Prompt提示词工程 Spring AI使用知识库增强对话功能 Spring AI实现一个智能客服 Spring AI实现一个简单的对话机器人 实现MinIO数据的每日备份 简单理解AI智能体 大模型和大模型应用 LangChain开篇 LangChain4j开篇 一个解析Excel2007的POI工具类 DataPermissionInterceptor源码解读 TenantLineInnerInterceptor源码解读
自己实现一个DNS服务
Liu Zijian · 2025-08-14 · via Liu Zijian's Blog | 一个技术博客

有时,我们所在单位的电脑只允许上内网,外网被断掉了,如果想要同时上内外网,我们可以通过修改路由表,然后双网卡一机两网的方式来实现分流上网,例如网线连公司内网,用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()