目的
在Anroid下完成TCP协议和UDP协议,了解HTTP请求过程。
请求流程
应用HTTP请求流程
下面是客户端发起HTTP请求大概流程,这里的虚拟网络是通过VPNService创建,HTTP的数据包都会写入虚拟网络,最后会根据IP和端口从虚拟网络拿到响应数据,这些操作都是应用那边的,我们不需要管,我们只要知道,他把HTTP请求的数据包写入虚拟网络,我们可以读出来,然后通过Socket连接写入数据,再读取Socket数据,按照规定协议写入虚拟网络中,应用他会自己读取出来。
JAVA应用流程
下面是JAVA应用大概流程,通过虚拟网络获取数据包,然后通过pcap4j解析数据包,通过源ip和源port从Map中获取TCP或UDP连接,根据协议不同走不同流程。TCP使用SocketChannel和UDP使用DatagramChannel两个NIO,设置非阻塞通过Selector去监听事件。
TCP协议中的SEQ计算
| 操作 | 计算 |
|---|---|
| 回应SYN | + 1 |
| 回应FIN | + 1 |
| 回应带data | + data.length |
| 其他 | + 0 |
遇到问题
- 多线程使用
Selector问题,一个线程先selector.select();(会阻塞线程),另一个线程后再执行channel.register(selector,...)就会阻塞当前线程,可以先使用selector.wakeup()唤醒第一个线程,再注册事件,大量数据执行还是会出现阻塞线程。 - 多线程使用FileChannel会出错。
- channel.write(buffer)写入数据不一定将buffer的数据写完。
最后效果
我是指定拦截com.android.browser(自带浏览器)应用。
代码操作
使用 Android Studio 创建一个项目,创建一个MyVpnService继承VpnService,setupVpn方法启动,builder设置基本参数,主要创建一个ParcelFileDescriptor对象,这个对象可以拿到数据流,进行读写。
if (descriptor == null) {
Builder builder = new Builder();
// 添加IPv4地址
builder.addAddress("10.0.0.2", 32);
// 添加IPv4路由,拦截所有ipv4
builder.addRoute("0.0.0.0", 0);
// 添加dns服务器
builder.addDnsServer("114.114.114.114");
try {
//指定应用拦截
//builder.addAllowedApplication("com.example.vpnservicedemo");
builder.addAllowedApplication("com.android.browser");
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
// ParcelFileDescriptor是一个文件描述符,它是一种程序读写已打开文件、socket的对象
descriptor = builder.setConfigureIntent(pendingIntent).establish();
//创建一个读取对象
readVpn = new ReadVpn(this, descriptor);
readVpn.start();
}
修改AndroidManifest.xml文件,添加权限声明,MyVpnService的service声明。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.VpnServiceDemo"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MyVpnService" android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
</application>
</manifest>
ReadVpn的start方法,读取数据并解析数据,pushPc.pushData()是将数据包通过socket方式推送到pc电脑上,好让Wireshark查看数据包情况,parseData解析数据包。
public void start() {
//创建一个线程读取channel数据
readDataThread = new Thread(() -> {
FileChannel readChannel = null;
ExecutorService threadPool = null;
try {
// 获取一个channel,零拷贝读取数据
readChannel = new FileInputStream(descriptor.getFileDescriptor()).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024 * 20);
while (!Thread.interrupted()) {
int len = readChannel.read(buffer);
if (len == -1) {
break;
}
if (len > 0) {
buffer.flip();
byte[] bt = new byte[len];
buffer.get(bt);
//计算接收数量
receiveLen += len;
//发送到ui
pushUi();
//推送PC
pushPc.pushData(bt);
parseData(bt);
buffer.clear();
}
}
} catch (Exception e) {
System.out.println("ReadData start 异常: " + e.getMessage());
} finally {
if (readChannel != null) {
try {
readChannel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if (threadPool != null) {
threadPool.shutdown();
}
}
});
readDataThread.setName("readDataThread");
readDataThread.start();
}
parseData 方法,IpSelector.newPacket解析数据包,根据源ip和源prot判断唯一,如果数据包是tcp就创建TcpPipe,udp就创建UdpPipe,然后再对应执行receive方法。
private void parseData(byte[] array) {
try {
Packet packet = IpSelector.newPacket(array, 0, array.length);
if (packet instanceof IpPacket) {
IpPacket ipPacket = (IpPacket) packet;
//通过源ip和源port判断唯一SendData
String key = getKey(ipPacket);
if (key != null) {
if (pipeMap.get(key) == null) {
Pipe pipe = null;
if (ipPacket.getHeader().getProtocol() == IpNumber.TCP) {
pipe = new TcpPipe(vpnService, this);
} else if (ipPacket.getHeader().getProtocol() == IpNumber.UDP) {
pipe = new UdpPipe(vpnService, this);
}
if (pipe != null) {
//防止多线程问题
pipeMap.putIfAbsent(key, pipe);
}
}
Pipe pipe = pipeMap.get(key);
int code = pipe.receive(ipPacket);
if (code == -1) {
//说明关闭了
pipeMap.remove(key);
}
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("ReadData parseData 解析包异常:" + e.getMessage());
}
}
TcpPipe.receive 方法,syn标志(第一次握手)创建SocketChannel.open()初始化属性,注册连接事件到Selecter,非syn标志有数据就写入SocketChannel,有fin标志就回应fin标志,并且关闭SocketChannel。
public int receive(IpPacket ipPacket) throws IOException {
TcpPacket tcpPacket = (TcpPacket) ipPacket.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 = ipPacket.getHeader().getDstAddr();
int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
int sequenceNumber = tcpPacket.getHeader().getSequenceNumber();
if (rst) {
close();
return -1;
}
if (sendFin.get() && replyFin.get()) {
close();
return -1;
}
if (syn) {
if (socketChannel == null) {
srcIpPacket = ipPacket;
replyAck.set(sequenceNumber + 1);
// 使用nio
socketChannel = SocketChannel.open();
// 使连接不被自身拦截
vpnService.protect(socketChannel.socket());
// 使用非阻塞io
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(dstAddr, dstPort));
//注册事件
TcpSelector.registerConnect(socketChannel, this);
}
} else {
if (socketChannel != null && !socketChannel.isConnected()) {
//连接重置
replyRst(tcpPacket);
return -1;
}
try {
Packet payload = tcpPacket.getPayload();
if ((payload != null && payload.getRawData().length > 0 ) || fin) {
int dataLen = 0;
if (payload != null && payload.getRawData().length > 0) {
dataLen = payload.getRawData().length;
}
if (dataLen > 0) {
if (socketChannel.isConnected()) {
ByteBuffer wrap = ByteBuffer.wrap(payload.getRawData());
while (wrap.hasRemaining()){
socketChannel.write(wrap);
}
}
}
int add = dataLen + (fin ? 1 : 0);
replyAck(replyAck.get() + add);
replyAck.addAndGet(add);
if (tcpPacket.getHeader().getFin()) {
replyFin.set(true);
if (sendFin.getAndSet(true)) {
replyFin();
}
socketChannel.close();
}
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("sendData 异常:" + e.getMessage());
close();
return -1;
}
}
return 0;
}