大部分路由器,CPE都会显示下挂设备列表,即lan侧设备。通常做法是通过读取arp表和DHCP租期文件,以获取lan侧设备的ip,mac,hostname等信息。但这种方式有两个弊端:
1 . arp表有老化时间,意味着arp表不是实时更新的,即可能出现设备已经断开,但arp表还有其IP和MAC的对应信息。导致显示错误。
2 . 通过读取DHCP租期文件获取数据,当设备为桥接模式时,会获取不到任何数据。
解决方法
arp老化时间
此问题可通过arping解决,从arp获取到lan侧设备IP,先通过arping(有的设备会禁ping,使用ping结果不一定准确)测试其是否实时连接,若arping不通,则说明此设备已断开连接。lan设备列表则不显示此IP。
桥接模式
桥接模式下获取不到数据是因为本地DHCP已关闭,LAN侧设备的IP是通过上级路由分配的,本地的DHCP租期文件无任何数据。
由于LAN侧设备与上级路由进行DHCP交互的时候,DHCP包会经过路由器LAN口,所以可通过在LAN口抓取DHCP包,解包获取其IP,MAC等信息。
具体实现
DHCP包格式详情DHCP包格式。
整体思路是添加内核模块,在网口侧抓DHCP Request包,再逐层解析数据包,最后从option中,解析出IP等信息。其中option 12是hostname,option 60是vendor,option 50是requested ip。
解包接口如下:
unsigned int apModeListDhcpHook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
unsigned int isGetIP = 0;
unsigned int isGetHostname = 0;
unsigned int isGetVendor = 0;
unsigned int unUdpLen = 0;
unsigned int currentIndex = 0;
unsigned int optionLen = 0;
unsigned char mac[MAX_MAC_SIZE] = "";
unsigned char hostname[MAX_STR_SIZE] = "";
unsigned char vendor[MAX_STR_SIZE] = "";
unsigned char ip[MAX_IP_SIZE] = "";
const unsigned char *ucUdpData = NULL;
const struct iphdr *iph = NULL;
const struct udphdr *uh = NULL;
// 没有in设备
if (unlikely(!state->in))
{
return NF_ACCEPT;
}
if (__constant_htons(ETH_P_IP) == skb->protocol)
{
iph = ip_hdr(skb);
if (unlikely(!iph))
{
return NF_ACCEPT;
}
// 非UDP协议
if (iph->protocol != IPPROTO_UDP)
{
return NF_ACCEPT;
}
// 非67端口
uh = (struct udphdr *) ((unsigned char *) iph + (iph->ihl << 2));
if(__constant_ntohs(67) != uh->dest)
{
return NF_ACCEPT;
}
// UDP数据开始部分
ucUdpData = (unsigned char *)((unsigned char *)uh + 8);
unUdpLen = ntohs(uh->len);
if (unUdpLen < DHCP_TYPE_OFFSET)
{
return NF_ACCEPT;
}
// 不是request包
if (ucUdpData[DHCP_TYPE_OFFSET] != DHCP_TYPE_REQUEST)
{
return NF_ACCEPT;
}
// 拷贝mac
if (0 == memcmp(ucUdpData + DHCP_CLIENT_MAC_OFFSET, FFMAC, MAX_MAC_SIZE))
{
return NF_ACCEPT;
}
memset(mac, 0, MAX_MAC_SIZE);
memcpy(mac, ucUdpData + DHCP_CLIENT_MAC_OFFSET, MAX_MAC_SIZE);
hostnameLen = 0;
vendorLen = 0;
memset(hostname, 0, MAX_STR_SIZE);
memset(vendor, 0, MAX_STR_SIZE);
memset(ip, 0, MAX_IP_SIZE);
// 开始遍历option, option是tlv结构,option 12 为 hostname, option 60 为 vendor, option 50 为 requested ip
currentIndex = DHCP_TYPE_OFFSET + 1;
while (currentIndex < unUdpLen)
{
if (12 == ucUdpData[currentIndex]) // option 12, can get hostname
{
optionLen = ucUdpData[++currentIndex];
hostnameLen = optionLen > MAX_STR_SIZE ? MAX_STR_SIZE : optionLen;
memcpy(hostname, ucUdpData + currentIndex + 1, hostnameLen);
isGetHostname = 1;
}
else if (50 == ucUdpData[currentIndex]) // option 50, can get client ip
{
optionLen = ucUdpData[++currentIndex];
memcpy(ip, ucUdpData + currentIndex + 1, optionLen > MAX_IP_SIZE ? MAX_IP_SIZE : optionLen);
isGetIP = 1;
}
else if (60 == ucUdpData[currentIndex]) // option 60, can get vendor
{
optionLen = ucUdpData[++currentIndex];
vendorLen = optionLen > MAX_STR_SIZE ? MAX_STR_SIZE : optionLen;
memcpy(vendor, ucUdpData + currentIndex + 1, vendorLen);
isGetVendor = 1;
}
else if (255 == ucUdpData[currentIndex]) // option 255, end
{
break;
}
else // 其他数据,偏移指针继续遍历
{
optionLen = ucUdpData[++currentIndex];
}
currentIndex += (optionLen + 1);
// ip 、hostname 、vendor都获取到了,提前退出
if (3 == isGetHostname + isGetIP + isGetVendor)
{
break;
}
}
// 有的设备没有字段,加上默认值
if (isGetHostname != 1)
{
hostnameLen = strlen("unknow-host");
memcpy(hostname, "unknow-host", hostnameLen);
}
if (isGetVendor != 1)
{
vendorLen = strlen("no-vendor");
memcpy(vendor, "no-vendor", vendorLen);
}
add_client_info_to_list(mac, hostname, vendor, ip);
}
return NF_ACCEPT;
}
执行结果如下: