普通视图

Received before yesterday

使用Java实现一个DNS服务

2025年8月14日 00:00

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

OA系统的天数该怎样计算

2024年12月31日 00:00

在开发一些OA系统的过程中,经常能遇到一个问题,就是时长计算,比如请假有请假的时长,出差有出差的时长,有的公司请假只能按照整天或小时为单位请假,这种都比较好处理,只要排除休息日节假日天数或排除下班非工作小时数直接相减即可,但是如果需求是按照半天为单位,OA系统该怎样计算总时长呢?

以半天为最小单位时,机械的加减有时可能无法和实际情况相符,例如我在OA系统提交休假审批,从1号上午到2号上午为假期,如果直接假定开始的小时都一样直接把日期时间简单相减,那么休假时间就是24小时构成的1天,但是实际上假期的构成是1号上午,1号下午,以及2号上午,按照工时的普遍计算逻辑就是1.5个工作日,OA系统应计算实际休假时长是1.5天,那这种场景下OA系统要怎样计算才准确呢,我把情况分为4种分别处理,分别计算休假一天,两天和三天的情况,进而推导到更长时间

1. 上午开始,上午结束

1天内的情况是0.5天,2天内的是1.5天,3天内是2.5天

2. 上午开始,下午结束

1天内的情况是1天,2天内的是2天,3天内是3天

3. 下午开始,下午结束

1天内的情况是0.5天,2天内的是1.5天,3天内是2.5天

4. 下午开始,上午结束

1天内不会有这种情况,2天内的是1天,3天内是2天

综上,计算的代码就是这样的:

public static void main(String[] args) {    System.out.println(test(            LocalDate.parse("2024-12-01"),            LocalDate.parse("2024-12-03"),            1,            1    ));}/** *  * @param start 开始日期 * @param end 结束日期 * @param startPeriod 开始 1上午2下午 * @param endPeriod 结束 1上午 2下午 * @return */public static double test(LocalDate start, LocalDate end, int startPeriod, int endPeriod) {    // 如果开始时间或结束时间为空,则返回 0 天    if (start == null || end == null) {        return 0.0; // 确保返回值类型一致    }    // 计算两个日期之间的整天数    double between = (double) java.time.temporal.ChronoUnit.DAYS.between(start, end);    //天数相减会把涉及的天数算少一天,所以要加回来    between ++;    // 根据时间段调整天数    if (startPeriod == 1) { // 开始是上午        if (endPeriod == 1) { // 结束是上午            between =  between - 0.5 ;        }    }    else if (startPeriod == 2) { // 开始是下午        if (endPeriod == 1) { // 结束是上午            between = between - 1;        }        else if (endPeriod == 2) { // 结束是下午            between = between - 0.5 ;        }    }    // 返回计算后的天数    return between;}

一些场景下,为了小数计算更加精确,可以使用java.math.BigDecimal进行计算

❌