使用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.1和127.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; }}
