阅读视图

使用Java实现一个DNS服务

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

IPv4和IPv6

原文地址:https://www.rockylinux.cn/notes/rocky-linux-9-network-configuration.html
原文作者:木子
Rocky Linux 中文社区欢迎您 https://www.rockylinux.cn

IPv4 与 IPv6

在进行 IP 配置之前,我们延伸了解一下 IPv4 与 IPv6 。 IPv4(Internet Protocol version 4)和 IPv6(Internet Protocol version 6)是互联网上用于数据包交换的两个版本的网络层协议。它们是互联网协议套件的核心部分,负责在网络设备之间路由和传递数据。

IPv4

IPv4 是第四版互联网协议,自 1981 年以来一直被广泛使用。IPv4 的特点包括: 地址空间: IPv4 使用 32 位地址,这意味着它可以支持大约 42 亿个独特的 IP 地址。 地址表示: IPv4 地址通常以点分十进制格式表示,例如 192.168.1.1。 地址配置: IPv4 地址可以手动配置(静态)或通过动态主机配置协议(DHCP)自动分配。 分片: IPv4 允许在传输过程中对数据包进行分片,这可以由发送端、接收端或中间路由器处理。 由于互联网的快速增长,IPv4 地址已经耗尽,这促使了对更广泛地址空间协议的需求。 在 IPv4 地址空间中,地址分为公网 IP、私有 IP 和 CGN(Carrier Grade NAT)地址。以下是详细区分:

公网 IP 地址

公网 IP 地址是全球唯一的,可以在整个互联网中进行通信的 IP 地址。它们不属于下列提到的私有 IP 和 CGN 地址的范围。所以,除了以下私有 IP、CGN 地址以及保留地址和特殊用途地址(如多播地址、环回地址等),其他的都属于公网 IP。

私有 IP 地址

私有 IP 地址用于局域网(LAN)内部通信,是不会在互联网中进行路由的。这些地址范围由 IANA(Internet Assigned Numbers Authority)分配:

  1. 10.0.0.0 到 10.255.255.255
  2. 172.16.0.0 到 172.31.255.255
  3. 192.168.0.0 到 192.168.255.255

CGN (Carrier Grade NAT) 地址

CGN 地址也称为共享地址空间,用于 ISP 提供的 NAT 方案,以减少 IPv4 地址的消耗。以下是该范围:

  1. 100.64.0.0 到 100.127.255.255

这些地址也不会在全球互联网中进行路由,用于解决多个用户共享一个公共 IP 地址的需求(Tailscale 用的这个地址段)。

其他特殊地址

还有一些保留和特殊用途的地址,例如:

  • 环回地址: 127.0.0.1
  • 广播地址: 255.255.255.255
  • 多播地址: 224.0.0.0 到 239.255.255.255

IPv6

IPv6 是互联网协议的最新版本,旨在解决 IPv4 地址耗尽的问题,并引入了一些新的特性和改进。IPv6 的特点包括:

  • 地址空间: IPv6 使用 128 位地址,极大地扩展了地址空间,可以支持近乎无限数量的独特 IP 地址。
  • 地址表示: IPv6 地址通常以冒号分隔的十六进制格式表示,例如 2001:0db8:85a3:0000:0000:8a 2 e:0370:7334。
  • 地址配置: IPv6 地址可以通过多种方式配置,包括静态配置、状态无关地址自动配置(SLAAC)和动态主机配置协议版本 6(DHCPv 6)。
  • 无分片: IPv6 设计时取消了路由器的分片功能,要求发送端执行路径最大传输单元(PMTU)发现,并发送适合路径上最小链路 MTU 的数据包。 在 IPv6 中,没有对应 IPv4 的私有 IP 和公网 IP 的概念,但有类似的机制来实现内网和公网的区别与应用。以下是一些重要的 IPv6 地址类型和其用途:

全球单播地址(Global Unicast Address)

全球单播地址就是 IPv6中用于在全球范围内进行通信的唯一地址,类似于 IPv4的公网 IP。其地址范围一般是以 2000::/3 开头。

唯一本地地址(Unique Local Address, ULA)

唯一本地地址在某种程度上类似于 IPv4的私有 IP 地址,用于局域网通信,不会在全球互联网中进行路由。其地址范围是 FC00::/7,也可以细分为以下两个范围:

  • 随机分配的 ULA: FD00::/8,通用情况下会使用这个范围,通过随机生成的方式保证在局部网络内的唯一性。
  • 原始分配的 ULA: FC00::/8,目前未正式广泛使用。

链路本地地址(Link-Local Address)

这些地址只能用于单个网络链路的节点之间,不能路由到其他链路。所有 IPv6 接口在启动时都会自动生成一个链路本地地址以支持邻居发现协议。其地址范围是 FE80::/10

不管是 Linux、macOS 还是 Windows 都会分配一个 Link-Local Address,以inet6 FE80::开头。

# Linux[root@localhost ~]# ifconfigens18: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500        inet 192.168.1.3  netmask 255.255.255.0  broadcast 192.168.1.255        inet6 fe80::486a:e224:31e4:d1fc  prefixlen 64  scopeid 0x20<link>        ether 52:ea:eb:77:3d:fe  txqueuelen 1000  (Ethernet)        RX packets 112410841  bytes 40294807433 (37.5 GiB)        RX errors 0  dropped 0  overruns 0  frame 0        TX packets 64910  bytes 24656852 (23.5 MiB)        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0 # macOS❯ ifconfigen0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500    options=400<CHANNEL_IO>    ether f8:28:19:6a:2b:0f    inet6 fe80::18cd:9189:ab4:ef40%en0 prefixlen 64 secured scopeid 0x6    inet 192.168.1.2 netmask 0xffffff00 broadcast 192.168.1.255    nd6 options=201<PERFORMNUD,DAD>    media: autoselect    status: active # WindowsPS C:\Users\muzi> ipconfigWindows IP 配置以太网适配器 以太网:    连接特定的 DNS 后缀 . . . . . . . :   本地链接 IPv6 地址. . . . . . . . : fe80::530b:7d8a:998a:f3f5%16   IPv4 地址 . . . . . . . . . . . . : 192.168.1.1   子网掩码  . . . . . . . . . . . . : 255.255.255.0   默认网关. . . . . . . . . . . . . : 192.168.1.254

其他类型地址

还有一些其他特殊用途的地址,比如:

  • 多播地址: FF00::/8,用于多播通信。
  • 组播地址: FF00::/8,用于组播通信。

IPv4 与 IPv6 之间的主要区别

  • 地址长度: IPv4 是 32 位,IPv6 是 128 位。
  • 地址表示法: IPv4 使用点分十进制,而 IPv6 使用冒号分隔的十六进制。
  • 地址空间: IPv6 提供了比 IPv4 更广阔的地址空间。
  • NAT 转换: 消除 NAT 以将地址空间从 32 位扩展到 128 位。
  • IPSec 支持: 在 IPv6 中,IPSec 是核心特性的一部分,但同样也需要进行配置,比如采用 strongswan 等。
  • 数据包处理: IPv6 简化了数据包头部,以提高路由效率,并取消了路由器分片功能。
  • 自动配置: IPv6 支持更高级的自动配置能力。
  • 多播和广播: IPv6 支持多播,但不支持 IPv4 那样的网络广播。取而代之,IPv6 使用多播和邻居发现协议来实现网络上的设备发现和配置。

IPv4 与 IPv6 这些区别反映了互联网协议在安全性、效率、可扩展性方面的进步,同时也提出了新的挑战,例如迁移和兼容性问题。随着 IPv6 逐渐被广泛采用,这些挑战将得到解决。

以下两图为 IPv4 与 IPv6 报文头对比:


  •