目的
如何让网络流量走自己的网络适配器呢,浏览器又不能设置选择网络适配器,在windows下可以修改hosts添加域名和临时ip映射,临时ip在网络适配器子网掩码内,就会走该网络适配器,在java中你还的弄个映射表去记录你的实际ip, 几个还好,但是多起来就麻烦。
具体实现
自己实现个dns服务器,记录实际ip返回临时ip,临时ip会走自建适配器,java socket时将临时ip变成实际ip,完成数据传输。
wintun创建适配器设置ip:172.29.1.1,子网掩码:255.255.0.0,dns地址:172.29.1.2
子网掩码范围255*255 个做临时ip够用,dns地址会走自建适配器,dns默认走udp端口53,我们在适配器中拦截就行了,就不用在弄个dns服务器。
udp协议
udp报文
sql
0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| Source | Destination |
| Port | Port |
+--------+--------+--------+--------+
| Length | Checksum |
+--------+--------+--------+--------+
|
| Data octets (if any)
|
+-------------------------------------+
Source Port (16 bits): 源端口号,占据UDP头部的前两个字节,指示发送方应用程序所使用的端口号。
Destination Port (16 bits): 目标端口号,占据UDP头部的第三和第四字节,指示接收方应用程序所使用的端口号。
Length (16 bits): 长度字段指示UDP数据报的长度,包括头部和数据部分的长度。长度字段的最小值是8(头部的长度),最大值是65535字节。
Checksum (16 bits): 校验和字段用于检测UDP数据报在传输过程中是否发生了错误。如果校验和字段为0,表示未使用校验和。
Data (Variable): 数据字段包含UDP数据报的实际数据内容。它的长度可以从Length字段中得知。
UDP报文相对于TCP报文而言非常简单,没有像TCP那样的序列号、确认号、窗口大小等字段,也没有复杂的连接管理机制。
pcap4j可以帮我计算Length和Checksum,没什么好关注字段,比tcp简单多了。
wintu构建一个dns服务器
dns可以使用udp协议实现,端口53 。如果udp 53端口超时,他会发送tcp 53端口的。
pcap4j 解析wintun获取的数据包,使用 UdpData 类解析udp
scss
//解析数据包
IpPacket packet = (IpPacket) IpSelector.newPacket(packetBytes, 0, packetBytes.length);
if(packet.getPayload() instanceof TcpPacket){
TcpData.parse(packet,sessionHandle);
}else if(packet.getPayload() instanceof UdpPacket){
UdpData.parse(packet,sessionHandle);
}
过滤一些端口协议和广播,防止数据包在网络一直循环
scss
InetAddress dstAddr = ipPacket.getHeader().getDstAddr();
UdpPacket udpPacket = (UdpPacket) ipPacket.getPayload();
int dstPort = udpPacket.getHeader().getDstPort().valueAsInt();
byte[] rawData = udpPacket.getPayload().getRawData();
//过滤一些协议
if (dstPort == 1900) {
//ssdp协议
remove(ipPacket);
return;
} else if (dstPort == 5355) {
//LLMNR
remove(ipPacket);
return;
} else if (dstPort == 5353) {
//MDNS
remove(ipPacket);
return;
} else if (dstAddr.getAddress()[3] == (byte) 255) {
//广播
remove(ipPacket);;
return;
}
双重校验DatagramChannel channel,没有创建,最后使用channel写入数据。复用channel 同一个源ip和源端口用同一个channel。使用本地网关192.168.1.1来替换172.29.1.2来访问dns服务。udp的DatagramChannel连接还得判断ip4和ip6,不知道为什么不通过connect时自己去判断。开启一个线程去读取udp那边返回的数据,然后写入wintun的网络适配器中(如果想减少线程可以使用Selector,会麻烦些,会有多线程问题),构建数据包使用pcap4j方便很多。
ini
//双重校验 volatile添加线程可见性
if (channel == null) {
synchronized (this) {
if (channel == null) {
try {
//判断是ip4还ip6
ProtocolFamily protocolFamily = StandardProtocolFamily.INET;
if (ipPacket instanceof IpV6Packet) {
protocolFamily = StandardProtocolFamily.INET6;
}
channel = DatagramChannel.open(protocolFamily);
if(dstAddr.getHostAddress().equals("172.29.1.2")){
dstAddr = InetAddress.getByName("192.168.1.1");
}
channel.connect(new InetSocketAddress(dstAddr, dstPort));
srcPacket = ipPacket;
readUdpThread = new Thread(() -> {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (!Thread.interrupted()) {
try {
int len = channel.read(buffer);
if (len > 0) {
buffer.flip();
byte[] array = new byte[buffer.limit()];
buffer.get(array);
//构建upd包
byte[] bytes = buildUdpPacket(ipPacket, array);
//写入网络适配器
writeData(bytes);
} else {
//认为连接关闭
close();
}
} catch (IOException e) {
e.printStackTrace();
close();
}
}
});
readUdpThread.setName(dstAddr + ":" + dstPort + " readUdpThread");
readUdpThread.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
try {
if(channel.isConnected()){
//写入数据
channel.write(ByteBuffer.wrap(rawData));
}
} catch (IOException e) {
e.printStackTrace();
close();
}
可以使用一下 dnsjava 测试一dns服务,java自带InetAddress不能设置dns服务器ip。
mavne依赖
xml
<dependency>
<groupId>dnsjava</groupId>
<artifactId>dnsjava</artifactId>
<version>3.5.3</version>
</dependency>
查询dns服务器
ini
public void dnsQuery() throws Exception {
Record queryRecord = Record.newRecord(Name.fromString("www.baidu.com."), Type.A, DClass.IN);
Message queryMessage = Message.newQuery(queryRecord);
Resolver r = new SimpleResolver("172.29.1.2");
r.sendAsync(queryMessage)
.whenComplete(
(answer, ex) -> {
if (ex == null) {
System.out.println(answer);
} else {
ex.printStackTrace();
}
})
.toCompletableFuture()
.get();
}
结果看没有问题
缓存临时ip和实际ip
生成临时ip:ip是4个byte组成,4个byte就是一个int,我们可以将172.29.1.3变成int开始,每次生成就+1,generateIp方法生成,bytesToInt方法和intToBytes方法由gpt生成。
scss
private static byte[] initIp = new byte[]{(byte)172,(byte)29,(byte)1,(byte) 3};
private static AtomicInteger number = new AtomicInteger(0);
public int generateIp(){
return bytesToInt(initIp)+number.getAndIncrement();
}
private static int bytesToInt(byte[] bytes) {
if (bytes.length != 4) {
throw new IllegalArgumentException("Byte array must have length of 4");
}
int result = 0;
for (int i = 0; i < 4; i++) {
result = (result << 8) | (bytes[i] & 0xFF);
}
return result;
}
private static byte[] intToBytes(int value) {
byte[] bytes = new byte[4];
bytes[0] = (byte) ((value >> 24) & 0xFF);
bytes[1] = (byte) ((value >> 16) & 0xFF);
bytes[2] = (byte) ((value >> 8) & 0xFF);
bytes[3] = (byte) (value & 0xFF);
return bytes;
}
使用 guava 中的Cache 缓存ip,key用ip转成的int
scss
static Cache<Integer, DnsData> dnsCache = CacheBuilder.newBuilder()
.maximumSize(1000) // 设置缓存最大容量
.expireAfterWrite(3, TimeUnit.MINUTES) // 设置写入后的过期时间
.build();
public static void putCache(InetAddress inetAddress){
int i = generateIp();
ipCache.put(i,inetAddress);
}
public static InetAddress getCache(int i){
return ipCache.getIfPresent(i);
}
在读取udp数据时判断一下ip是172.29.1.2,然后修改一下ip,具体updateDnsIp方法
scss
while (!Thread.interrupted()) {
try {
int len = channel.read(buffer);
if (len > 0) {
buffer.flip();
byte[] array = new byte[buffer.limit()];
buffer.get(array);
//判断是dns 服务
if (srcPacket.getHeader().getDstAddr().getHostAddress().equals("172.29.1.2")) {
array = updateDnsIp(array);
}
//构建upd包
byte[] bytes = buildUdpPacket(ipPacket, array);
//写入网络适配器
writeData(bytes);
} else {
//认为连接关闭
close();
}
} catch (Exception e) {
e.printStackTrace();
close();
}
}
我们主要修改的是dns 的Answers 或者Additional Info 的 type = A
updateDnsIp方法看起来代码好多,实际上就是修改type=A的数据(A表示的是ip4地址),然后重新构造数据包
scss
private byte[] updateDnsIp(byte[] array) throws IllegalRawDataException {
DnsPacket dnsPacket = DnsPacket.newPacket(array, 0, array.length);
DnsPacket.DnsHeader header = dnsPacket.getHeader();
String domain = null;
if (header.getQdCount() > 0) {
DnsQuestion dnsQuestion = header.getQuestions().get(0);
domain = dnsQuestion.getQName().getName();
System.out.println(domain);
}
//(附加信息)部分
List<DnsResourceRecord> answersList = new ArrayList<>();
for (DnsResourceRecord answer : header.getAnswers()) {
if (answer.getDataType() == DnsResourceRecordType.A) {
DnsRDataA dnsRDataA = (DnsRDataA) answer.getRData();
byte[] bytes = IpCache.putCache(dnsRDataA.getAddress());
DnsRDataA dnsRDataA1 = DnsRDataA.newInstance(bytes, 0, bytes.length);
DnsResourceRecord build = new DnsResourceRecord.Builder()
.name(answer.getName())
.dataType(answer.getDataType())
.ttl(answer.getTtl())
.rData(dnsRDataA1)
.dataClass(answer.getDataClass())
.correctLengthAtBuild(true).build();
answersList.add(build);
} else {
answersList.add(answer);
}
}
//(附加信息)部分
List<DnsResourceRecord> additionalList = new ArrayList<>();
for (DnsResourceRecord info : header.getAdditionalInfo()) {
if (info.getDataType() == DnsResourceRecordType.A) {
DnsRDataA dnsRDataA = (DnsRDataA) info.getRData();
byte[] bytes = IpCache.putCache(dnsRDataA.getAddress());
DnsRDataA dnsRDataA1 = DnsRDataA.newInstance(bytes, 0, bytes.length);
DnsResourceRecord build = new DnsResourceRecord.Builder()
.name(info.getName())
.dataType(info.getDataType())
.ttl(info.getTtl())
.rData(dnsRDataA1)
.dataClass(info.getDataClass())
.correctLengthAtBuild(true).build();
additionalList.add(build);
} else {
additionalList.add(info);
}
}
DnsPacket build = new DnsPacket.Builder()
.id(dnsPacket.getHeader().getId())
.response(dnsPacket.getHeader().isResponse())
.opCode(dnsPacket.getHeader().getOpCode())
.authoritativeAnswer(dnsPacket.getHeader().isAuthoritativeAnswer())
.truncated(dnsPacket.getHeader().isTruncated())
.recursionDesired(dnsPacket.getHeader().isRecursionDesired())
.recursionAvailable(dnsPacket.getHeader().isRecursionAvailable())
.reserved(dnsPacket.getHeader().getReservedBit())
.authenticData(dnsPacket.getHeader().isAuthenticData())
.checkingDisabled(dnsPacket.getHeader().isCheckingDisabled())
.rCode(dnsPacket.getHeader().getrCode())
.questions(dnsPacket.getHeader().getQuestions())
.answers(answersList)
.authorities(dnsPacket.getHeader().getAuthorities())
.additionalInfo(additionalList)
.qdCount(dnsPacket.getHeader().getQdCount())
.anCount(dnsPacket.getHeader().getAnCount())
.nsCount(dnsPacket.getHeader().getNsCount())
.arCount(dnsPacket.getHeader().getArCount()).build();
return build.getRawData();
}
使用dnsjava测试一下
调整了一下之前的tcp那块代码,socket阻塞io换成 AsynchronousSocketChannel 异步io,在TcpData的 receive方中替换一下实际ip
ini
if(dstAddr instanceof Inet4Address){
InetAddress cache = IpCache.getCache(dstAddr);
if(cache!=null){
//更换实际ip
dstAddr = cache;
}
}
具体代码:wintun-03 · 断续/learn-demo - 码云 - 开源中国 (gitee.com)
测试结果
我这边使用Hyper-V创建一个ubuntu虚拟机,修改dns服务器为:172.29.1.2
使用浏览器访问测试一下,发现没有问题。具体数据包信息可以通过Wireshark查看。