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

推荐订阅源

月光博客
月光博客
Martin Fowler
Martin Fowler
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Last Watchdog
The Last Watchdog
S
Schneier on Security
C
Cisco Blogs
P
Privacy International News Feed
T
Tenable Blog
Spread Privacy
Spread Privacy
Recent Commits to openclaw:main
Recent Commits to openclaw:main
N
News and Events Feed by Topic
Security Archives - TechRepublic
Security Archives - TechRepublic
阮一峰的网络日志
阮一峰的网络日志
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
大猫的无限游戏
大猫的无限游戏
Project Zero
Project Zero
GbyAI
GbyAI
N
Netflix TechBlog - Medium
T
Tor Project blog
雷峰网
雷峰网
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
T
Threat Research - Cisco Blogs
Cyberwarzone
Cyberwarzone
L
LangChain Blog
MyScale Blog
MyScale Blog
C
CERT Recently Published Vulnerability Notes
C
Check Point Blog
G
Google Developers Blog
T
Tailwind CSS Blog
L
LINUX DO - 热门话题
宝玉的分享
宝玉的分享
IT之家
IT之家
F
Fortinet All Blogs
TaoSecurity Blog
TaoSecurity Blog
Recent Announcements
Recent Announcements
T
The Exploit Database - CXSecurity.com
Hacker News: Ask HN
Hacker News: Ask HN
aimingoo的专栏
aimingoo的专栏
云风的 BLOG
云风的 BLOG
Engineering at Meta
Engineering at Meta
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Google Online Security Blog
Google Online Security Blog
Help Net Security
Help Net Security
H
Hacker News: Front Page
小众软件
小众软件
U
Unit 42
Apple Machine Learning Research
Apple Machine Learning Research
P
Privacy & Cybersecurity Law Blog
T
Threatpost

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源码解读 BaseMultiTableInnerInterceptor源码解读 Spring AI开篇 SQL解析工具JSQLParser 芋道源码解读之多租户 芋道源码解读之数据权限 芋道源码解读开篇 Java实现将数据导出为Word文档 OA系统的天数该怎样计算 安装MySQL8 安装MySQL5.7 RockyLinux9环境下编译MySQL8 MySQL字符集及底层原理 Java实现LDAP登录 Docker Compose IPv4和IPv6 使用虚拟机安装一个K8s集群 使用GraalVM原生编译打包SpringBoot工程 Nginx防止目录穿越 Java线程的状态 Nginx防盗链设置 使用python将excel表格转换为SQL INSERT Redis的公共操作命令 Redis数据结构之Bitfleid Redis数据结构之Bitmap Redis数据结构之GEO Redis数据结构之Hash Redis数据结构之HyperLogLog Redis数据结构之List Redis数据结构之Set Redis数据结构之Stream Redis数据结构之String Redis数据结构之ZSet 使用python压缩图片 利用Python实现Hexo站点的持续集成 Nginx设置HTTPS监听 firewalld防火墙工具的使用 Linux信号(signal)机制 MySQL5.7x 主从复制 用IP自签发一个HTTPS证书 基于Hexo实现一个静态的个人博客 RockyLinux9环境下编译MySQL5.7 Docker离线安装 MySQL数据定义语言 Docker与联合文件系统 Docker的网络 Docker的镜像操作 MySQL存储过程 MyBatis-Plus开篇 MySQL变量 MySQL视图 MySQL事务 MySQL插入修改和删除 MySQL查询 MySQL系统命令 Docker的容器操作 Docker的安装和配置 Docker容器数据卷 浅谈OAuth2.0授权原理 JVM开篇 浅谈Linux(Unix)的I/O模型 一个通用的CloseableHttpClient工厂类 JUC可重入锁ReentrantLock JUC读写锁ReadWriteLock Java的单例 Java泛型 Java8的新特性 最近最少使用算法(LRU) MySQL函数 SpringBoot配置和启动 volatile作用分析
自己实现一个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()