netlink通信——读取路由表获取通信网卡IP

读取路由表获取通信网卡IP是什么意思呢?且听我一一道来...

下面是我虚拟机两个网卡的IP,很明显两个网卡是不同网段的,我的物理机网卡网段是192.168.1.0/24,与我物理机和外网通信的网卡是ens160,即192.168.31.0/24网段,ens192不能和物理机与外网通信。

我的程序运行是这样的:

运行了两次,第一次是读取路由表获取到192.168.1.4(我主机)的路由,然后根据路由确定用哪个网卡进行通信,网卡ens160网络设置的是NAT,所以和物理机不在一个网段但是可以通信。第二次是读取路由表获得到主机192.168.2.3的路由,然后根据路由确定用哪个网卡进行通信,可以看到这次网卡选择的是ens192(192.168.2.1)。虽然局域网中没有192.168.2.3这个主机,但不影响我说明本程序的功能:读取路由表获取到目的主机的路由,根据路由选择用哪个网卡进行通信。

程序流程如下:

其中创建Routing Socket代码:

c 复制代码
route_sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)

第一个参数是协议族,AF_NETLINK即是netlink协议族。第二个参数一般固定为SOCK_RAW,第三个参数是netlink协议族,NETLINK_ROUTE表示获取或管理路由表信息。

sockaddr_nl结构体为netlink地址结构:

c 复制代码
struct sockaddr_nl {
	__kernel_sa_family_t	nl_family;	/* AF_NETLINK	*/
	unsigned short	nl_pad;		/* zero		*/
	__u32		nl_pid;		/* port ID	*/
       	__u32		nl_groups;	/* multicast groups mask */
};

其初始化:

c 复制代码
	bzero(&sa, sizeof(sa));
	// nl_pady一般为0
	// nl_groups == 0 表示该消息为单播
	sa.nl_family = AF_NETLINK;
	sa.nl_pid = getpid();  // nl_pid表示接收消息者的进程ID

netlink消息结构大概如下:

上面的图对于理解代码很重要,关于构造netlink消息、发送netlink消息等可以直接看代码,中有注释。

netlink_getip.h

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <bits/sockaddr.h>
#include <asm/types.h>
#include <linux/rtnetlink.h>
#include <sys/socket.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <netdb.h>
#include <unistd.h>
#include <string.h>

#define BUFFER_LENGTH 8192
typedef struct rt_request
{
	struct nlmsghdr nl;
	struct rtmsg rt;
	char payload[BUFFER_LENGTH];
} rt_request;

uint32_t fetch_interface_ip(uint32_t if_index)
{
	int family;
	struct ifreq ifreq;
	char host[256] =
	{ 0 }, if_name[256] =
	{ 0 };
	uint32_t src_addr;
	int fd;

	if_indextoname(if_index, if_name);  // 根据索引值获取网络接口名,如eth0
	fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd < 0)
	{
		perror("socket()");
		exit(EXIT_FAILURE);
	}

	memset(&ifreq, 0, sizeof ifreq);
	strncpy(ifreq.ifr_name, if_name, IFNAMSIZ);
	if (ioctl(fd, SIOCGIFADDR, &ifreq) != 0)    // 获取接口ip
	{
		/* perror(name); */
		return -1; /* ignore */
	}

	switch (family = ifreq.ifr_addr.sa_family)
	{
	case AF_UNSPEC:
		// return;
		return -1; /* ignore */
	case AF_INET:
	case AF_INET6:
		getnameinfo(&ifreq.ifr_addr, sizeof ifreq.ifr_addr, host, sizeof host,
				0, 0, NI_NUMERICHOST);
		break;
	default:
		sprintf(host, "unknown  (family: %d)", family);
	}
	inet_pton(AF_INET, host, &src_addr);
	close(fd);
	return src_addr;
}

void formRequest(rt_request* req)
{
	bzero(req, sizeof(req));
/*
struct nlmsghdr 为 netlink socket 自己的消息头,
这用于多路复用和多路分解 netlink 定义的所有协议类型以及其它一些控制,
netlink 的内核实现将利用这个消息头来多路复用和多路分解已经其它的一些控制,
因此它也被称为netlink 控制块。因此,应用在发送 netlink 消息时必须提供该消息头。
*/
	req->nl.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
	req->nl.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;  // NLM_F_REQUEST表示消息是一个请求
	req->nl.nlmsg_type = RTM_GETROUTE;  // nlmsg_type消息内容

    // 填充rtmsg结构体,即路由表管理结构体,对于上面的RTM_GETROUTE操作来说,只需要定义下面两个内容
	req->rt.rtm_family = AF_INET;
	req->rt.rtm_table = RT_TABLE_MAIN;

}

void sendRequest(int sock_fd, struct sockaddr_nl *pa, rt_request* req)
{
	struct msghdr msg;    // sendmsg和recvmsg的参数,描述发送消息和接收消息的结构体
	struct iovec iov;     // iovec结构体用于描述一个数据缓冲区
	int rtn;

	bzero(pa, sizeof(pa));
	pa->nl_family = AF_NETLINK;

	bzero(&msg, sizeof(msg));
	msg.msg_name = pa;
	msg.msg_namelen = sizeof(*pa);

	iov.iov_base = (void *) req;
	iov.iov_len = req->nl.nlmsg_len;

	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

	while (1)
	{
		if ((rtn = sendmsg(sock_fd, &msg, 0)) < 0)
		{
			if (errno == EINTR)
				continue;
			else
			{
				printf("Error: Unable to send NetLink message:%s\n",
						strerror(errno));
				exit(1);
			}
		}
		break;
	}

}

int receiveReply(int sock_fd, char* response_buffer)
{
	char* p;
	int nll, rtl, rtn;
	struct nlmsghdr *nlp;
	struct rtmsg *rtp;

	bzero(response_buffer, BUFFER_LENGTH);
	p = response_buffer;
	nll = 0;

	while (1)
	{
		if ((rtn = recv(sock_fd, p, BUFFER_LENGTH - nll, 0)) < 0)
		{
			if (errno == EINTR)
				continue;
			else
			{
				printf("Failed to read from NetLink Socket: %s\n",
						strerror(errno));
				exit(1);
			}

		}

		nlp = (struct nlmsghdr*) p;
		if (nlp->nlmsg_type == NLMSG_DONE)
			break;

		p += rtn;
		nll += rtn;
	}
	return nll;
}

uint32_t readReply(char *response, int nll, in_addr_t dst_address)
{
	struct nlmsghdr *nlp = NULL;
	struct rtmsg *rtp = NULL;
	struct rtattr *rtap = NULL;
	int rtl = 0, found_route = 0, default_route = 0;
	uint32_t route_addr, net_mask;
	uint32_t if_index = -1;

	nlp = (struct nlmsghdr*) response;
	for (; NLMSG_OK(nlp, nll); nlp = NLMSG_NEXT(nlp, nll))  // NLMSG_OK:检查nlh地址是否是一条完整的消息
	{                                                       // NLMSG_NEXT:当前消息地址,返回下一个消息地址
		rtp = (struct rtmsg *) NLMSG_DATA(nlp);        // NLMSG_DATA:从nlh首地址向后移动到data起始位置

		if (rtp->rtm_table != RT_TABLE_MAIN)
			continue;

		// RTM_RTA:输入route message指针,返回route第一个属性首地址
		rtap = (struct rtattr *) RTM_RTA(rtp);    // rtattr结构体封装可选路由信息的通用结构,用于表示 Netlink 消息的属性
		rtl = RTM_PAYLOAD(nlp);    // RTM_PAYLOAD:即rtmsg层封装的数据长度,相当于TCP数据包去掉IP报头和TCP报头长度得到TCP数据部分长度
		found_route = 0;
		default_route = 1;

		for (; RTA_OK(rtap, rtl); rtap = RTA_NEXT(rtap, rtl))  // RTA_OK:判断一个属性rta是否正确
		{                                                      // RTA_NEXT:先对attrlen减去rta属性内容的全部长度,然后返回下一个rtattr的首地址
			switch (rtap->rta_type)
			{
			// destination IPv4 address
			case RTA_DST:
				default_route = 0;
				route_addr = *((uint32_t*) RTA_DATA (rtap));
				net_mask = 0xFFFFFFFF;
				net_mask <<= (32 - rtp->rtm_dst_len);
				net_mask = ntohl(net_mask);
				if (route_addr == (dst_address & net_mask))
					found_route = 1;
				else if (route_addr == 0)
					default_route = 1;
				break;

				// unique ID associated with the network
				// interface
			case RTA_OIF:  // Output interface index
				if (found_route || default_route)
					if_index = *((uint32_t*) RTA_DATA (rtap));
				break;

			default:
				break;
			}
		}

		if (found_route)
			break;
	}

	return if_index;

}
// Netlink分层模型及消息格式:https://onestraw.github.io/linux/netlink-message/
uint32_t getLocalIPAddress(in_addr_t dst_address)
{
	int route_sock_fd = -1, res_len = 0;
	struct sockaddr_nl sa, pa;    // sa为消息接收者的 netlink 地址
	uint32_t if_index;

	rt_request req = {0};
	char response_payload[BUFFER_LENGTH] = {0};

	// Open Routing Socket
	if ((route_sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) == -1)
	{
		printf("Error: Failed to open routing socket: %s\n", strerror(errno));
		exit(1);
	}

	bzero(&sa, sizeof(sa));
	// nl_groups == 0 表示该消息为单播
	sa.nl_family = AF_NETLINK;
	sa.nl_pid = getpid();  // nl_pid表示接收消息者的进程ID

	bind(route_sock_fd, (struct sockaddr*) &sa, sizeof(sa));

	formRequest(&req);    // 构造netlink消息
	sendRequest(route_sock_fd, &pa, &req);    // 发送消息
	res_len = receiveReply(route_sock_fd, response_payload);    // 接收消息
	if_index = readReply(response_payload, res_len, dst_address);  // 从接收的消息中获取if(network interface)

	close(route_sock_fd);
	return fetch_interface_ip(if_index);    // 从if_index获取接口ip
}

get_ip.c

c 复制代码
#include "netlink_getip.h"

int main(int argc, char** argv)
{
	char dst[256] = {0}, host[256] = {0};

	if (argc != 2)
	{
		printf("Usage: ./rawhttpget ip\n");
		exit(1);
	}

	strncpy(dst, argv[1], 256);

    // netlink通信
	uint32_t src_address = getLocalIPAddress(inet_addr(dst));

	printf("Src Address: %s\n", inet_ntop(AF_INET, &src_address, host, 256));

	return 0;
}

Makefile

c 复制代码
CFLAGS= -g -Werror
CC=gcc

all:
	$(CC) get_ip.c $(CFLAGS) -o netlink_getip

clean:
	rm -rf netlink_getip

文章参考:

https://github.com/praveenkmurthy/Raw-Sockets

https://onestraw.github.io/linux/netlink-message/

欢迎交流。

相关推荐
龙哥说跨境24 分钟前
如何利用指纹浏览器爬虫绕过Cloudflare的防护?
服务器·网络·python·网络爬虫
懒大王就是我39 分钟前
C语言网络编程 -- TCP/iP协议
c语言·网络·tcp/ip
Elaine2023911 小时前
06 网络编程基础
java·网络
海绵波波1072 小时前
Webserver(4.3)TCP通信实现
服务器·网络·tcp/ip
幺零九零零5 小时前
【计算机网络】TCP协议面试常考(一)
服务器·tcp/ip·计算机网络
热爱跑步的恒川5 小时前
【论文复现】基于图卷积网络的轻量化推荐模型
网络·人工智能·开源·aigc·ai编程
云飞云共享云桌面6 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
音徽编程8 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
幺零九零零9 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
23zhgjx-NanKon10 小时前
华为eNSP:QinQ
网络·安全·华为