MENU

【代码札记】使用JNA在Windows上创建并操作TUN设备

January 18, 2023 • 瞎折腾

虽然大家一致认为Java不适合做底层,但我觉得不适合不代表不能做。

本篇文章比起札记,其实更像是一篇show off。

背景

最近我在看去中心系统相关的内容,看到了Yggdrasil,且不说它的原理如何,它在使用方式上就让我耳目一新。传统的overlay network都需要通过某种应用层的接口来让用户访问,比如Tor和I2P都需要浏览器支持,后者更是提供了诸如socks这种代理协议来提供访问,这就要求应用程序必须专门考虑并为此设计,才能获得一定程度的兼容性。对于不支持socks协议的程序,很遗憾,它没法接入I2P网络。

Yggdrasil不同:它在运行时创建了一个TUN设备。TUN设备是一种虚拟网络适配器,类似TAP。TAP设备工作在二层,换言之它能够得到的是数据链路层的数据包(更细致地说,其实是媒体访问控制MAC包),而TUN设备则工作在三层,它能够得到的数据是IP包。Yggdrasil将网络中的每一个节点的公钥映射成IPv6地址中被废弃的一段0020::0/7,这段地址在可观的未来不会被使用(但Yggdrasil网络中节点的公钥是256位,而这个地址段只有121位,我还不太清楚他们是怎么解决地址冲突的),每个节点分配一个段内的IPv6地址,这样一来,只要在操作系统中访问这个段内的地址,不管你是ICMP还是TCP还是UDP,操作系统都封装成IP包,交给这个TUN设备。而Yggdrasil从这个TUN设备中获取封装好的IP号,看一下目的地地址,在网络内路由,到达对方节点后,直接通过TUN设备写入操作系统的IP栈,之后无论是什么协议,全部由操作系统负责处理和分发。多么优雅的解决方案啊!

但是只有一个遗憾,这个软件是Go写的,而我是写Java的。我翻遍了Google和GitHub,也没有发现支持Java在Windows上创建和操作TUN设备的库。我很不爽,于是就自己写了一个。

自己造轮子

经过一番搜索,我发现了一个叫做wintun的C++库,这个库将win32 API的调用封装成了简单的函数,诸如WintunCreateAdapter这种。要知道,让Java调用C就已经很困难了,你需要处理好Java对象和C结构体之间的映射,要处理好指针、类型的问题。而微软的Win32为了让事情变得更加复杂(其实是历史遗留问题),他们使用了自己的一套类型系统,并且有者自己的一套API设计理念,用起来更加麻烦。

看到有前人栽树,现在后人就得想怎么乘凉。好在Java调用C并不是什么稀奇事情:除了原生的JNI之外,我们还有JNA和JNR,还有未来的Project Panama。就目前而言,对于这个项目来讲,JNA是最好的选择。关于编写mapping的过程就不再赘述了,无非就是参照JNA文档,遇到问题了Google一下。不过其中有个小插曲,就是无论怎么调试,一开始得到的结果返回的都是null,我百思不得其解啊。最后无意中想到,这个玩意儿是不是需要管理员权限?但是IDEA并没有提供单独将程序作为管理员运行的选项,不得已,我只能将整个IDEA作为管理员运行。对于来路不明的软件,这个动作其实非常危险:不光是IDEA,由IDEA启动的Gradle也是作为管理员运行的,如果构建脚本里有一些恶意代码,使用管理员权限运行刚好给了他们可乘之机。不过考虑到这个是我自己写的软件,顶多蓝屏,能怎么样?

编写好了Mapping,我发现一个问题:这个软件只能创建和操作TUN设备,但是没办法给TUN设备分配IP。虽然我可以在TUN设备中模拟DHCP包,但是这未免有些复杂且太不灵活了。查阅了一大堆文档,最后发现win32 API还是绕不开了。于是开始想办法编写相关的win32 API。具体内容可以参考这个项目的GitHub Repo

因为是C,免不了很多面向过程编程。与其一开始考虑怎么把这个东西做的尽可能OOP,我的建议是每次针对一个功能(例如列出IP,插入MIB ROW等),写一个demo,确保能用之后,再将这个功能封装成对象的成员方法。不然一边设计对象,一边考虑C的过程,实属给自己找不痛快。

展示

经过了一周的艰苦奋斗,这个库终于是成了。为了验证效果,我借助pcap4j实现了ICMP ECHO(也就是ping)响应。效果就是,运行程序,创建一个TUN设备,程序为这个TUN设备分配IP为0020::100/7,并开始处理IP包。这个时候在系统内ping任意一个同网段的ip,都可以得到相对应的ping响应。

总体流程还是简单明了的:

package info.skyblond.jna

import com.sun.jna.platform.win32.Guid
import info.skyblond.jna.wintun.*
import org.pcap4j.packet.*
import org.pcap4j.packet.namednumber.IcmpV6Code
import org.pcap4j.packet.namednumber.IcmpV6Type
import org.pcap4j.packet.namednumber.IpNumber
import org.pcap4j.packet.namednumber.IpVersion
import java.io.EOFException
import java.net.Inet6Address
import kotlin.concurrent.thread
import kotlin.experimental.and
import kotlin.random.Random


fun main(args: Array<String>) {
    println("Current wintun version: ${WintunLib.INSTANCE.WintunGetRunningDriverVersion()}")
    val guid = Guid.GUID.newGuid().toGuidString()
    val adapter = WintunAdapter("Wintun Demo Adapter", "Wintun", guid)
    // Ring size: 8MB
    val session = adapter.newSession(0x800000)

    try { // clean up old ip
        adapter.dissociateIp(ip)
    } catch (t: Throwable) {
        t.printStackTrace()
    }
    val ip = Inet6Address.getByName("0020::100")
    println("Set ip to: $ip")
    adapter.associateIp(
        AdapterIPAddress(ip = ip, prefixLength = 7u)
    )

    TODO("Handle IP packets")

    println("Closing!")
    session.close()
    adapter.close()
}

整体流程就是创建网卡、启动Session、设定IP、处理IP包,最后关闭资源。

处理IP包的部分可以单独放在一个线程里:

    val t = thread {
        try {
            while (true) {
                val result = session.readPacket()
                if (result != null) thread { handlePacket(session, result) }
            }
        } catch (e: EOFException) {
            e.printStackTrace()
        } catch (e: NativeException) {
            e.printStackTrace()
        }
    }
    while (t.isAlive) {
        Thread.sleep(1000)
    }

由于读取的时候不一定总会读出数据,因此需要判断一下读出的内容是否为空,不为空再处理。处理起来也相对简单,无非是解析包,拿到目的地IP,然后创建一个回复的包,写回操作系统:

    private fun handlePacket(session: WintunSession, packet: ByteArray) {
        val isV6 = packet[0].and(0xf0.toByte()) == 0x60.toByte()
        println(
            "Get IPv${if (isV6) "6" else "4"} packet from OS\n" +
                    "\tSize: ${packet.size} bytes\n"
        )

        if (isV6) {
            val v6Packet = IpV6Packet.newPacket(packet, 0, packet.size)
            println(v6Packet)
            (v6Packet.payload as? IcmpV6CommonPacket)?.let { icmpV6Common ->
                (icmpV6Common.payload as? IcmpV6EchoRequestPacket)?.let { request ->
                    val reply = IpV6Packet.Builder()
                        .version(IpVersion.IPV6)
                        .trafficClass(IpV6SimpleTrafficClass.newInstance(0x00))
                        .flowLabel(IpV6SimpleFlowLabel.newInstance(0))
                        .srcAddr(v6Packet.header.dstAddr)
                        .dstAddr(v6Packet.header.srcAddr)
                        .nextHeader(IpNumber.ICMPV6)
                        .hopLimit(127)
                        .correctLengthAtBuild(true)
                        .payloadBuilder(
                            IcmpV6CommonPacket.Builder()
                                .srcAddr(v6Packet.header.dstAddr)
                                .dstAddr(v6Packet.header.srcAddr)
                                .type(IcmpV6Type.ECHO_REPLY)
                                .code(IcmpV6Code.NO_CODE)
                                .correctChecksumAtBuild(true)
                                .payloadBuilder(
                                    IcmpV6EchoReplyPacket.Builder()
                                        .identifier(request.header.identifier)
                                        .sequenceNumber(request.header.sequenceNumber)
                                        .payloadBuilder(request.payload.builder)
                                )
                        )
                        .build()
                    println("Reply: \n$reply")
                    session.writePacket(reply.rawData)
                }
            }
        } else {
            val v4Packet = IpV4Packet.newPacket(packet, 0, packet.size)
            println(v4Packet)
        }
    }

程序运行起来之后ping 0020::300,效果如下:

无标题.png

关于完整的程序和这个库,可以在GitHub上找到。

-全文完-


知识共享许可协议
【代码札记】使用JNA在Windows上创建并操作TUN设备天空 Blond 采用 知识共享 署名 - 非商业性使用 - 相同方式共享 4.0 国际 许可协议进行许可。
本许可协议授权之外的使用权限可以从 https://skyblond.info/about.html 处获得。

Archives QR Code
QR Code for this page
Tipping QR Code