Java实现Docker全局代理加速

231 阅读5分钟

背景

我们加速一个网站访问,往往使用的是代理,代理有HTTP代理,HTTPS代理,SOCKS5代理。但是这些代理往往需要软件支持,有的软件不支持呢,还有控制台的curl、pip等命令呢。我下面就是分析实现这个过程,可能有更好的方式。

HTTP流程图

微信截图_20250412022159.png

思路

首先,我们想管理网络的流量,就需要创建网卡(网络适配器),然后通过路由将流量分发到我们创建的网卡上,最后在网卡上完成流量的交互。

如图上的HTTP流程图,我们想让他走指定的网卡,需要弄一个DNS服务,还得系统默认走这个指定的DNS服务。

可以这样操作,创建一个网卡(ip:172.40.1.1,子网掩码:255.255.255.0),再创建一个DNS服务,当发现域名是www.baidu.com的时候,解析为172.40.1.2那他就走这个创建的网卡了,对应在将www.baidu.com的映射存起来,给网卡那边使用。

网卡(网络适配器)

创建网卡(网络适配器)使用wintun(windows环境下)

流量到了网卡,网卡是得到流量包的形式,包含:TCP包,UDP包,ICMP包等。

解析TCP数据包,完成TCP协议,通过Java的Socket发送数据包。

这里使用Java的Soket,不折腾AIO和NIO了,主要Socket支持代理(HTTP代理,HTTPS代理,Socks代理)。

Socket瓶颈是一个Socket就会阻塞一个线程,现在有虚拟线程的加持,就不会阻塞线程了。

TCP协议

微信截图_20250412021841.png

下面是用Wireshark抓的tcp协议包(curl http://example.com)

192.168.1.3	23.192.228.80	66	TCP	60952 → 80 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 WS=256 SACK_PERM
23.192.228.80	192.168.1.3	66	TCP	80 → 60952 [SYN, ACK] Seq=0 Ack=1 Win=64240 Len=0 MSS=1452 SACK_PERM WS=128
192.168.1.3	23.192.228.80	54	TCP	60952 → 80 [ACK] Seq=1 Ack=1 Win=65280 Len=0
192.168.1.3	23.192.228.80	129	HTTP	GET / HTTP/1.1 
23.192.228.80	192.168.1.3	60	TCP	80 → 60952 [ACK] Seq=1 Ack=76 Win=64256 Len=0
23.192.228.80	192.168.1.3	119	HTTP	[TCP Previous segment not captured] Continuation
23.192.228.80	192.168.1.3	1506	TCP	[TCP Out-Of-Order] 80 → 60952 [ACK] Seq=1 Ack=76 Win=64256 Len=1452
192.168.1.3	23.192.228.80	66	TCP	[TCP Dup ACK 772#1] 60952 → 80 [ACK] Seq=76 Ack=1 Win=65280 Len=0 SLE=1453 SRE=1518
192.168.1.3	23.192.228.80	54	TCP	60952 → 80 [ACK] Seq=76 Ack=1518 Win=65280 Len=0
192.168.1.3	23.192.228.80	54	TCP	60952 → 80 [FIN, ACK] Seq=76 Ack=1518 Win=65280 Len=0
23.192.228.80	192.168.1.3	60	TCP	80 → 60952 [FIN, ACK] Seq=1518 Ack=77 Win=64256 Len=0
192.168.1.3	23.192.228.80	54	TCP	60952 → 80 [ACK] Seq=77 Ack=1519 Win=65280 Len=0

代码

1.创建网络适配器Test,并且设置IP和子网掩码。

String name = "Test";
String tunnelType = "Wintun";
//创建网络适配器
Pointer adapterHandle = WintunLibrary.INSTANCE.WintunCreateAdapter(new WString(name), new WString(tunnelType), null);

if (adapterHandle == null) {
    throw new RuntimeException("网络适配器创建失败!(可能是没用管理员启动)");
}

String hostAddress = "172.40.1.1";
//设置网络设配器ip 和 子网掩码
WintunExtensionLibrary.INSTANCE.CreateAddressRow(adapterHandle,hostAddress,16);

//启动适配器
Pointer sessionHandle = WintunLibrary.INSTANCE.WintunStartSession(adapterHandle, 0x400000);

2.创建DNS服务,并且设置网络适配器Test的DNS地址(ipv4和ipv6)。


//创建dns
String ipv4 = "127.0.0.1";
String ipv6 = "::1";
SimpleDnsServer ssV4 = new SimpleDnsServer(ipv4,hostAddress);
executor.submit(ssV4);

SimpleDnsServer ssV6 = new SimpleDnsServer(ipv6,hostAddress);
executor.submit(ssV6);
//修改dns
WindowsDnsManager.setManualDNSv4(name, ipv4); // 修改
WindowsDnsManager.setManualDNSv6(name, ipv6); // 修改
//刷新dns缓存
WindowsDnsManager.flushDns();


// 注册关闭钩子,程序结束自动还原 DNS
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    try {
        WindowsDnsManager.setAutoDNSv4(name);
        WindowsDnsManager.setAutoDNSv6(name);
    } catch (Exception e) {
        e.printStackTrace();
    }
}));

3.设置docker.com顶级域名,解析成自定义IP,11行

public class SimpleDnsServer implements Runnable {

    private static final int DNS_PORT = 53;
    private final InetAddress bindAddress;
    private final InetAddress toAddress;

    private final Resolver defaultResolver;

    public static final BiMap<String,InetAddress> biMap = new BiMap<>();
    private final List<String> allowedTlds = Arrays.asList("docker.com");

    private ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();


    public SimpleDnsServer(String bindAddress,String toAddress) {
        try {
            this.bindAddress = InetAddress.getByName(bindAddress);
            this.toAddress = InetAddress.getByName(toAddress);
            this.defaultResolver = new SimpleResolver("8.8.8.8");
            this.defaultResolver.setTCP(false);
            this.defaultResolver.setPort(53);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    ...(省略代码)
}

4.TCP协议解析。

....(省略代码)
public void parsePacket(IpPacket packet) {
    try {
        TcpPacket tcpPacket = (TcpPacket) packet.getPayload();

        boolean syn = tcpPacket.getHeader().getSyn();
        boolean fin = tcpPacket.getHeader().getFin();
        boolean psh = tcpPacket.getHeader().getPsh();
        boolean ack = tcpPacket.getHeader().getAck();
        boolean rst = tcpPacket.getHeader().getRst();

        InetAddress dstAddr = packet.getHeader().getDstAddr();
        int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();

        int sequenceNumber = tcpPacket.getHeader().getSequenceNumber();


        InetAddress srcAddr = packet.getHeader().getSrcAddr();
        int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
        
        String key = srcAddr + "#" + srcPort;
        TcpSocket tcpSocket = hashMap.get(key);
        //第一次握手
        if (syn) {
            if (tcpSocket == null) {
                tcpSocket = new TcpSocket(dstAddr, dstPort,packet,sequenceNumber);
                hashMap.put(key, tcpSocket);
                tcpSocket.connection();
            }
        }else {

            //服务重启导致的
            if(tcpSocket == null){
                new TcpSocket(dstAddr, dstPort,packet,sequenceNumber).replyRst(tcpPacket);
                return;
            }

            //连接重置
            if(rst){
                hashMap.remove(key);
                tcpSocket.close();
                return;
            }

            //第四次挥手
            if(ack && tcpSocket.sendFin.get() && tcpSocket.replyFin.get()){
                hashMap.remove(key);
                tcpSocket.close();
                return;
            }
            //写入队列
            tcpSocket.writeQueue.offer(tcpPacket);

        }
    }catch (Exception ex){
        ex.printStackTrace();
    }

}
...(省略代码)

5.创建Socket代理连接,使用socks5代理,因为他支持域名访问。下面是用的socks5:127.0.0.1:9000代理,读写任务都是用虚拟线程跑。


...(省略代码)
public void connection() {
    try {
        Proxy proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("127.0.0.1", 9000));
        socket = new Socket(proxy);
//                socket.setSoTimeout(5000);
        //转换域名
        String domain = SimpleDnsServer.biMap.getKey(dstAddr);
        //不需要解析域名
        SocketAddress remoteAddr = InetSocketAddress.createUnresolved(domain, dstPort);
        socket.connect(remoteAddr);
S
        replySyn();

        //读任务
        readTask();
        //写任务
        writeTask();
    } catch (IOException e) {
        e.printStackTrace();
        try {
            socket.close();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}
...(省略代码)

测试

条件:需要能访问docker.com的Socks5代理服务。当然你自己可以部署一个Socks5代理服务,可以看我之前文章,docker.com可能会被拦截,你换成百度,看样子是访问百度的。

通过cmd简单去curl https://hub.docker.com,可以看到超时了。

C:\Users\22787>curl https://hub.docker.com
curl: (28) Failed to connect to hub.docker.com port 443 after 21026 ms: Could not connect to server

启动Java程序,再执行,可以看到成功返回了。

C:\Users\22787>curl https://hub.docker.com/
<!DOCTYPE html><html lang="en" style="font-size:14px" data-hydrated="false"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&amp;display=swap"/><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&amp;family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&amp;display=swap"/><link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Comfortaa:300,400,400i,600,600i,700"/><script nonce="">window.__ssr = JSON.parse('{"csrf":"&O#j&t5q)^jJUqP*i^dMVsvy","speedyCache":true}');</script><script nonce="">(function() {

遇到的问题

DNS解析优先走跳跃点低的网络适配器,创建wintun跳跃点会比以太网的低。

DNS解析优先走的是IPv6

创建wintun的网络适配器需要设置IPv6和IPv4的DNS地址。

完整代码

wintun-04 · 断续/learn-demo - 码云 - 开源中国