本文章将只围绕坑与几个技术难点与WOL的一些相关知识展开,不会过多的描述具体细节,如若想知道还请自行查阅项目源码。
前言
本文仅供学习探讨之用,如果侵犯了您的权益请联系我删除。
工具
基本原理
核心依赖的东西只有一个,就是WOL(Wake-on-LAN)技术,即允许通过网络消息远程打开或唤醒网络中的某台计算机。
WOL技术依赖于主板与网卡,想要使用此功能必须确保你的设备支持此技术。就现代计算机来说,应该已经没有不支持的机器了吧?(心虚)
WOL唤醒的关键点是一个叫做MagicPacket(魔术包)
的东西,当网卡接收到发送往此网卡的魔术包并且经过校验后确认无误后就会通知主板进行系统的唤醒。
MagicPacket
的构造十分的简单,由前缀6
个0xFF
跟后面的重复16
次的6
个字节的目标机器的网卡MAC地址构成。你可以使用任何传输层的协议进行发送魔术包到目标主机,但通常会使用UDP
协议。
// MAC地址为 11:22:33:44:55:66 的示例魔术包
00000000 FF FF FF FF FF FF 11 22 33 44 55 66 11 22 33 44 ......."3DUf."3D
00000010 55 66 11 22 33 44 55 66 11 22 33 44 55 66 11 22 Uf."3DUf."3DUf."
00000020 33 44 55 66 11 22 33 44 55 66 11 22 33 44 55 66 3DUf."3DUf."3DUf
00000030 11 22 33 44 55 66 11 22 33 44 55 66 11 22 33 44 ."3DUf."3DUf."3D
00000040 55 66 11 22 33 44 55 66 11 22 33 44 55 66 11 22 Uf."3DUf."3DUf."
00000050 33 44 55 66 11 22 33 44 55 66 11 22 33 44 55 66 3DUf."3DUf."3DUf
00000060 11 22 33 44 55 66 ."3DUf
我们只需要将此魔术包发往目标机器即可实现远程唤醒机器。
参考资料:
需求分析
其实只实现远程唤醒很简单,上面的内容我觉得也已经说的比较清晰了,就是组包与发包的过程。
但是如果我们要写成APP的话就只有个唤醒功能肯定是不够的。毕竟不可能每次进入APP还要手动输入一次MAC地址IP地址吧,而且配置也很麻烦。所以我们需要一个扫描局域网在线设备的功能,并可以保存配置到本地,免去手动配置的麻烦。但同时我们也需要保留手动配置的选项作为最终解决手段。
此外还有一个问题就是如果我们的电脑的IP是动态获取的(DHCP),则我们的配置可能会失效,例如我现在的一个配置他的目标IP地址为192.168.1.233
,而真实记录的IP地址为192.168.1.234
。当我们往配置中的IP地址发送魔术包时由于真实机器并没有接收到,所以不会发生任何事情,于是我们需要一个可以控制是否精准唤醒目标机器的开关选项,如果是非精准唤醒则我们会向局域网广播地址192.168.1.255
发送魔术包,使得目标机器即使与配置IP不同也可以接收到我们发送的唤醒魔术包。
在局域网扫描中我们需要获取的信息有IP地址、MAC地址、主机名。其中主机名将作为默认的配置名称,IP地址与MAC地址为唤醒目标机器的主要参数。
技术难点
- 如何扫描局域网中的在线机器,并获取在线机器的IP地址与MAC地址。
- 如何通过目标机器的IP地址获取到目标机器的主机名。
解决方案
- 使用
UDP
协议往此网段中的1-254
台机器发送数据包使其产生ARP缓存,然后通过cat /proc/net/arp
指令获取ARP表,解析ARP表即可获得IP地址与MAC地址。 - 使用
LLMNR
协议协议通过IP地址反向查询目标机器的主机名。
通过IP地址获取主机名功能的实现
在Linux中有一款叫nslookup
的工具,其功能为通过IP地址可反向查询出目标机器的主机名,接下来我们将通过对这款工具的分析来搞清楚如何实现我们需要的功能。
打开我们的Ubuntu(Linux子系统 WSL),输入nslookup ip
即可查看到目标机器的主机名
可以看到成功查询出了主机名。
Wireshark
打开Wireshark,选择以太网开始监听,在filter一栏中我们输入dns || llmnr
过滤我们只需要看的协议。
回到控制台再一次执行我们刚才的命令
数据出来了之后我们停止抓包,来分析一下它获取主机名的流程。在这里我们只关注IPV4
的东西,IPV6
可以忽略不看。
可以看到他先是向默认的DNS服务器发送了一个反向查询主机名的请求(毕竟如果能直接在DNS服务器上找到就不需要进行多播了),但是DNS服务器上并没有找到相关的信息,所以它返回的数据包中并没有包含Answer
。
.in-addr.arpa
是反向查询的专属命名空间,例如图中查询192.168.3.102
则它的Name为102.3.168.192.in-addr.arpa
。
在查询DNS服务器无果后它又使用LLMNR协议向局域网中广播了一则”寻机启示”,其中224.0.0.252
这个地址是LLMNR协议的固定地址,而查询的数据包内容是不变的,因为LLMNR是基于DNS协议的一个小分支。
在广播了”寻机启示”之后,视网络状况而定目标主机如果接收到了信息,他就会主动向本机发送主机名等信息。
如图所示
其中红色框起来的是发送地址与目标地址,可以看到结果是由我们要查询的目标机器主动发往我们本机的。橙色框圈起来的Domain Name
就是我们要查询的目标机器的主机名。
至此问题解决。
代码实现
对于DeviceDiscovery.getComputerName
的实现
fun getComputerName(targetIp: String): String {
var domainNameOffset: Int
// 组包
val packet = targetIp.let {
val ips = it.split(".")
val hard01 = byteArrayOf(0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
val hard02 = byteArrayOf(0x00, 0x0c, 0x00, 0x01)
val packetBuilder = ByteBuffer.allocate(256)
packetBuilder.putShort(888) //标识符
packetBuilder.put(hard01) //硬编码01
// 编码查询地址
domainNameOffset = packetBuilder.position()
for (i in 3 downTo 0) {
packetBuilder.put(ips[i].length.toByte())
packetBuilder.put(ips[i].toByteArray())
}
packetBuilder.put(7)
packetBuilder.put("in-addr".toByteArray())
packetBuilder.put(4)
packetBuilder.put("arpa".toByteArray())
packetBuilder.put(0)
packetBuilder.put(hard02) //硬编码02
domainNameOffset = packetBuilder.position() - domainNameOffset + 6 + packetBuilder.position()
DatagramPacket(packetBuilder.array(), 0, packetBuilder.position(), InetAddress.getByName("224.0.0.252"), 5355)
}
// 发送请求
kotlin.runCatching { sender.send(packet) }.onFailure { return "UnReachableDevice" }
// 接收返回
val recvBuffer = ByteBuffer.allocate(1024)
val recvPacket = DatagramPacket(recvBuffer.array(), recvBuffer.capacity())
sender.soTimeout = 1000
kotlin.runCatching {
sender.receive(recvPacket)
}.onFailure { return "UnKnowDevice" }
// 读取域名
recvBuffer.position(domainNameOffset)
val domainNameByteArray = ByteBuffer.allocate(recvBuffer.get().toInt())
recvBuffer.get(domainNameByteArray.array())
return String(domainNameByteArray.array())
}
对于DeviceDiscovery.scan
的实现
fun scan(networkSegmentPrefix: String, progress: (Float) -> Unit = {}): ArrayList<LanDeviceInfo> {
val result = ArrayList<LanDeviceInfo>()
val data = ByteArray(1)
progress(0f)
for (i in 1..254) {
kotlin.runCatching { sender.send(DatagramPacket(data, 0, 1, InetAddress.getByName(networkSegmentPrefix + i), 888)) }
progress(i / 254f)
}
progress(1f)
progress(0f)
val table = runCommand("cat", "/proc/net/arp")!!
.replace("\r", "")
.split("\n")
for ((i, row) in table.withIndex()) {
val col = row.let {
val results = ArrayList<String>()
var flag = true
val sb = StringBuilder()
for (c in it.trim()) {
when (c) {
' ' -> {
if (flag) {
results.add(sb.toString())
sb.clear()
flag = false
}
}
else -> {
sb.append(c)
flag = true
}
}
}
if (flag)
results.add(sb.toString())
results
}
when (col.size) {
6 -> {
if ("00:00:00:00:00:00" != col[3] && col[0].contains(networkSegmentPrefix)) {
result.add(LanDeviceInfo(
getComputerName(col[0]),
col[0],
col[3],
col[5],
"888"
))
}
}
}
progress(i / table.size.toFloat())
}
progress(1f)
return result
}
最终效果
完整代码仓库地址
避坑指南
- 扫描是扫描局域网中目前在线的机器。
- 对于
真·关机
的机器是没法唤醒的,例如使用shutdown -s -t 0
进行关机或者长按电源键强制关机与断电导致关机的,这三种情况无法唤醒。 - Android10+的系统无法使用扫描功能,因为从Android10开始应用无法访问
/proc/net
,所以导致无法读取ARP表来进行获取IP地址与MAC地址。
一些思考
- 在配置中添加修改配置功能。
- 在配置中添加配置分组功能。
- 在配置中添加批量唤醒某个分组机器的功能。
- 寻找Android10+无法扫描的解决方案。
算是遗留问题了,懒狗不想实现
结语
那就这样了,有缘再见~