背景
我们加速一个网站访问,往往使用的是代理,代理有HTTP代理,HTTPS代理,SOCKS5代理。但是这些代理往往需要软件支持,有的软件不支持呢,还有控制台的curl、pip等命令呢。我下面就是分析实现这个过程,可能有更好的方式。
HTTP流程图
思路
首先,我们想管理网络的流量,就需要创建网卡(网络适配器),然后通过路由将流量分发到我们创建的网卡上,最后在网卡上完成流量的交互。
如图上的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协议
下面是用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&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&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&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地址。