阅读视图

使用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;    }}
  •  

Java实现LDAP登录

LDAP的全称是Lightweight Directory Access Protocol(轻量级目录访问协议),是一种用于访问和管理分布式目录信息服务的应用协议。LDAP通常用于存储用户、组和其他组织信息,提供对这些信息的快速查询和管理。

LDAP是基于X.500标准的一个简化版本,使用更简单的网络协议(如 TCP/IP)来实现,定义了客户端如何与目录服务交互,如添加、删除、修改或查询目录信息。

LDAP使用SSL加密(ldaps://)时,如果服务端是自签证书,需提前安装证书到jdk的信任证书库内,我采用的open-jdk8的证书库位于/etc/pki/ca-trust/extracted/java/cacerts,将自签发的证书certificate.pem导入

keytool -importcert -keystore $JAVA_HOME/jre/lib/security/cacerts -storepass changeit -trustcacerts -file certificate.pem -alias uua01

Java原生支持LDAP协议,通过管理员账户adminDnadminPassword连接LDAP服务器,并搜索用户的DN,验证用户凭据,再检查输入的密码是否正确

import javax.naming.Context;import javax.naming.NamingEnumeration;import javax.naming.NamingException;import javax.naming.directory.SearchControls;import javax.naming.directory.SearchResult;import javax.naming.ldap.Control;import javax.naming.ldap.InitialLdapContext;import javax.naming.ldap.LdapContext;import java.io.InputStream;import java.util.Hashtable;import java.util.Map;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class LdapVerify {    public boolean connehct(String username, String password) {        String ip = "";        String port = "";        String timeOut = "";        String adminDn = "";        String adminPassword = "";        String url = String.format("ldaps://%s:%s", ip, port);        // 1. 建立与 LDAP 的连接        Hashtable<String, String> env = new Hashtable<>();        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");        env.put(Context.PROVIDER_URL, url);        env.put(Context.SECURITY_AUTHENTICATION, "simple");        env.put(Context.SECURITY_PRINCIPAL, adminDn);        env.put(Context.SECURITY_CREDENTIALS, adminPassword);        env.put(Context.SECURITY_PROTOCOL, "ssl"); // 启用 LDAPS        env.put("com.sun.jndi.ldap.connect.timeout", "3000");        try {            LdapContext ldapContext = new InitialLdapContext(env, null);            // 2. 查找用户的完整 DN            String searchBase = "OU=All Users,DC=demo,DC=com"; // 搜索起点            String searchFilter = "(sAMAccountName=" + username + ")"; // 根据用户名查找            SearchControls searchControls = new SearchControls();            searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);            NamingEnumeration<SearchResult> results = ldapContext.search(searchBase, searchFilter, searchControls);            if (results.hasMore()) {                SearchResult result = results.next();                String userDn = result.getNameInNamespace();                log.info("LDAP登录, 找到用户 DN: " + userDn);                // 3. 验证用户密码                Hashtable<String, String> userEnv = new Hashtable<>();                userEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");                userEnv.put(Context.PROVIDER_URL, url);                userEnv.put(Context.SECURITY_AUTHENTICATION, "simple");                userEnv.put(Context.SECURITY_PRINCIPAL, userDn);                userEnv.put(Context.SECURITY_CREDENTIALS, password);                userEnv.put(Context.SECURITY_PROTOCOL, "ssl");                userEnv.put("com.sun.jndi.ldap.connect.timeout", "3000");                try {                    new InitialLdapContext(userEnv, null).close();                    log.info("LDAP登录, 用户验证成功 {}", username);                    return true;                }                catch (Exception e) {                    log.error("LDAP登录, 用户验证失败", username);                    return false;                }            }            log.error("LDAP登录, 找不到用户 DN {} ", username);            return false;        }        catch (NamingException e) {            log.error("LDAP登录, 找用户异常 DN {} {} ", username, e.getMessage(), e);            return false;        }    }}
  •  

MinIO

未完待续

1.概述

MinIO是一个对象存储解决方案,它基于GO语言编写,提供了与Amazon S3兼容的API,并支持部署在服务器,Docker或K8s。

2.服务端

2.1 二进制文件部署服务端

minio server 命令启动MinIO服务器. 这个路径参数/opt/miniodata确定服务器操作的文件夹。

nohup ./minio server   /opt/miniodata  \--config-dir /opt/minioconfig  \--console-address ":13399" \-address ":13487"   > minio.log &

2.2 Docker部署服务端

docker run -d \-p 3399:3399 \-p 3487:3487 \--name minio \-e "MINIO_ACCESS_KEY=12345678" \-e "MINIO_SECRET_KEY=12345678" \-v /opt/minio/data:/data \-v /opt/minio/config:/root/.minio \minio/minio server /data --console-address ":3399" -address ":3487"

3.客户端

MinIO客户端mc命令行工具提供了一个现代化的替代方案, 支持文件系统和与Amazon S3兼容的云存储服务,适用于UNIX命令如 ls 、 cat 、 cp 、 mirror 和 diff 。

客户端命令的基本格式:

mc [GLOBALFLAGS] COMMAND --help

3.1 下载

3.2 管理客户端 admin

测试连接,myminio为之前通过alias命令设置好的某个服务端的别名

[root@QNXGXUUAOAW012 opt]# ./mc admin info myminio●  127.0.0.1:13487   Uptime: 10 months    Version: 2024-07-31T05:46:26Z   Network: 1/1 OK    Drives: 1/1 OK    Pool: 1┌──────┬───────────────────────┬─────────────────────┬──────────────┐│ Pool │ Drives Usage          │ Erasure stripe size │ Erasure sets ││ 1st  │ 16.0% (total: 91 GiB) │ 1                   │ 1            │└──────┴───────────────────────┴─────────────────────┴──────────────┘773 MiB Used, 1 Bucket, 656 Objects1 drive online, 0 drives offline, EC:0

3.3 别名 alias

alias set将目标服务端http://127.0.0.1:13487设置为本地的别名myminio

[root@QNXGXUUAOAW012 opt]# ./mc alias set myminio http://127.0.0.1:13487  19j**********5UmjZW   zl0Fnit****************J8ANektAdded `myminio` successfully.[root@QNXGXUUAOAW012 opt]# 

3.4 拷贝文件 cp

递归拷贝服务端的uni******siaBucket(桶)下/2025/10/16路径下的所有文件到本地/opt/backup目录

[root@QNXGXUUAOAW012 opt]# ./mc cp myminio/uni******sia/2025/10/16 /opt/backup --recursive...e865ea0bb3c698e394b85ab3.png: 61.36 KiB / 61.36 KiB ┃▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓┃ 4.63 MiB/s 0s[root@QNXGXUUAOAW012 opt]#
  •  

浅谈OAuth2.0授权原理

一、引言

OAuth(Open Authorization)是一种开放授权协议,允许用户授权第三方应用访问其在其他服务中的资源(如个人信息、照片等)目前的版本是2.0版(OAuth 2.0),其标准是:RFC 6749

OAuth在客户端与服务提供商之间,设置了一个授权层(authorization layer)客户端不能直接登录服务提供商,只能登录授权层获取令牌,以此将用户与客户端区分开来。客户端登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期,客户端登录授权层以后,服务提供商根据令牌的权限范围和有效期,向客户端开放用户储存的资料。

很多场景都有采用OAuth2登录,例如登录ProcessOn或知识星球时可以使用微信登陆,登陆时会先跳转到微信的二维码页面,扫码登录后会再跳转到ProcessOn或知识星球,此时显示的是用户微信的昵称和头像,再比如使用GitHub登录Vercel时,也是先跳到GitHub的登录页面,输入用户名密码登陆后,跳转到Vercel,Vercel里面除了能展示用户GitHub的头像用户名等信息之外,还能获取用户GitHub中的仓库信息,甚至可以获取仓库中各分支下的源代码。这样就实现了脱离第三方系统进行认证,避免了用户直接将自己在微信等服务商的账户密码告诉ProcessOn等第三方平台进行获取数据所导致的安全问题。在这些场景下,上述的的客户端指的就是Vercel/知识星球/ProcessOn等第三方系统,服务提供商就是微信/GitHub。

OAuth2.0规定了四种获得令牌的流程和授权方式:

  • 授权码(authorization code)
  • 隐藏式(implicit),又叫简化模式
  • 密码式(password)
  • 客户端凭证(client credentials)

二、名词定义

在详细讲解OAuth 2.0之前,需要了解几个专用名词

  • Resource Owner 资源所有者,本文中又称用户(user)

  • User Agent 用户代理,本文中就是指浏览器。

  • Third-party application 第三方应用程序,又称客户端

  • HTTP service HTTP服务提供商,本文中简称服务提供商

  • Authorization server 认证服务器,即服务提供商专门用来处理认证的服务器。

  • Resource server 资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。

三、授权方式和原理

3.1 授权码模式(authorization code)

授权码模式(authorization code)是功能最完整、流程最严密的一种模式。它的特点就是通过客户端的后台服务器,与服务提供商的认证服务器进行互动,而且这种场景很常见,是toc场景下使用最多的OAuth授权模式,下面以微信扫码登陆知识星球为例具体说明:

  1. 知识星球要求用户给予授权可以理解为点击微信登陆,浏览器弹出微信的二维码登录页面,扫码后微信APP上会得到一个授权许可页面
  2. 知识星球用户在微信APP的授权许可页面点击了同意用微信登录知识星球,随即微信授权服务器生成授权码并使得浏览器上的扫码登陆页面跳转到知识星球的微信登录链接(这个链接在开发者在微信开放平台注册应用时就要提前填写好)并通过在这个链接上追加URL参数的方式传把授权码给到了知识星球平台
  3. 知识星球平台将接收到的微信登录授权码作为参数,后台调用微信认证服务器的获取令牌接口
  4. 微信认证服务器验证授权码通过,返回令牌给知识星球平台
  5. 知识星球平台紧接着使用令牌访问微信的资源服务器,获取微信登录用户的微信数据(昵称,ID,甚至步数等)
  6. 微信资源服务器验证令牌通过,返回用户的微信数据(昵称,ID,甚至步数等)给知识星球平台,知识星球平台根据用户的微信ID建立会话,到此就微信登录成功了,知识星球平台一般就会跳转到首页,还会在首页上展示用户的微信昵称头像等个人标识。

3.2 隐藏式(implicit)

隐藏式,又称简化模式(implicit grant type),不通过第三方应用程序的服务器,而是直接在浏览器中向认证服务器申请令牌,跳过了授权码这个步骤,因此得名。所有步骤可以在浏览器中完成,令牌对访问者是可见的。

和授权码模式不一样的是,客户端引导用户打开服务提供商认证页面,通过扫码或用户名密码认证后,跳转到指定的客户端系统页面,并直接在URL参数上传递访问令牌,省去了通过授权码再去获取令牌的过程。

3.3 密码式(password)

密码模式中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向服务商提供商索要授权。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

3.4 客户端凭证(client credentials)

客户端模式指客户端以自己的名义,而不是以用户的名义,向服务提供商进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求服务提供商提供服务,其实不存在授权问题。

四、参考

  1. 理解OAuth 2.0, 阮一峰, 2014.5
  2. OAuth 2.0 的一个简单解释, 阮一峰, 2019.04
  •