如何在端口112接收VRRP组播数据包?排查接收失败问题
问题描述
我正在做keepalived与VRRP协议的实验:计算机A运行keepalived,想在计算机B上读取VRRP组播消息,获取A通告的虚拟IP,两台设备连同一交换机。
我写的组播套接字代码能正常接收mDNS(224.0.0.251:5353)数据,但接收VRRP组播地址及端口(224.0.0.18:112)的消息时,程序会卡在recvfrom处。
已经用tcpdump确认计算机B确实能收到这些消息:
sudo tcpdump -i enp39s0 net 224.0.0.18
知道112是特权端口,所以用sudo运行程序。netstat显示端口已打开且已加入组播组:
$ netstat -lu | grep 112; netstat -g | grep vrrp udp 0 0 0.0.0.0:112 0.0.0.0:* enp39s0 2 vrrp.mcast.net
Stack Overflow上同类问题的方案是设置IP_ADD_MEMBERSHIP,但这不是当前问题所在。请问我忽略了什么?
以下是接收组播消息的代码(简化后,移除错误处理,Linux平台,Windows等效代码也有同样问题):
#include <cstring> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #define VRRP_MULTICAST_ADDR "224.0.0.18" // VRRP multicast address #define VRRP_PORT 112 // VRRP default port //#define VRRP_MULTICAST_ADDR "224.0.0.251" //#define VRRP_PORT 5353 int main() { sockaddr_in addr; ip_mreq mreq; char buffer[256]; int sockfd = socket(AF_INET, SOCK_DGRAM, 0); unsigned int opt_on = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (char*) &opt_on, sizeof(opt_on)); memset(&addr, 0, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(VRRP_PORT); bind(sockfd, (sockaddr *)&addr, sizeof(addr)); mreq.imr_multiaddr.s_addr = inet_addr(VRRP_MULTICAST_ADDR); mreq.imr_interface.s_addr = htonl(INADDR_ANY); setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)); ssize_t nbytes = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL); printf("Received %ld bytes\n", nbytes); // HexDump(buffer, nbytes); close(sockfd); return 0; }
问题排查与解决
你犯了一个核心概念错误:VRRP不是基于UDP的协议。你混淆了IP协议号和UDP端口号——VRRP的IP协议号是112,而非使用UDP端口112。你的UDP套接字根本无法接收到VRRP消息,因为这些消息本质是IP层数据包,不是UDP包。
tcpdump能抓到是因为它直接抓取IP层流量,而你的代码监听的是UDP端口,两者处理的是不同层级的数据包。
修正方案:使用原始套接字接收VRRP包
要接收VRRP消息,需要创建原始套接字,并指定接收IP协议号为112的数据包。修改后的代码如下:
#include <cstring> #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <unistd.h> #include <netinet/ip.h> #define VRRP_PROTOCOL 112 // VRRP的IP协议号 #define VRRP_MULTICAST_ADDR "224.0.0.18" // VRRP报文头部简化结构 struct vrrp_header { uint8_t version_type; uint8_t vrid; uint8_t priority; uint8_t count_ip; uint16_t auth_type; uint16_t adver_int; uint32_t checksum; // 后续为虚拟IP列表,此处简化 }; int main() { // 创建原始套接字,指定接收VRRP协议包 int sockfd = socket(AF_INET, SOCK_RAW, VRRP_PROTOCOL); if (sockfd < 0) { perror("socket creation failed"); return 1; } // 可选:绑定到指定接口,避免接收其他接口的无关流量 // const char* iface = "enp39s0"; // setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, iface, strlen(iface)); char buffer[1024]; sockaddr_in src_addr; socklen_t addr_len = sizeof(src_addr); // 接收VRRP数据包 ssize_t nbytes = recvfrom(sockfd, buffer, sizeof(buffer), 0, (sockaddr*)&src_addr, &addr_len); if (nbytes < 0) { perror("recvfrom failed"); close(sockfd); return 1; } printf("Received %ld bytes from %s\n", nbytes, inet_ntoa(src_addr.sin_addr)); // 解析IP头部 iphdr* ip_hdr = (iphdr*)buffer; int ip_hdr_len = ip_hdr->ihl * 4; // IP头部长度(单位:字节) // 解析VRRP头部 vrrp_header* vrrp_hdr = (vrrp_header*)(buffer + ip_hdr_len); printf("VRRP Version/Type: 0x%02X, VRID: %d, Priority: %d\n", vrrp_hdr->version_type, vrrp_hdr->vrid, vrrp_hdr->priority); // 提取虚拟IP(假设仅存在一个虚拟IP) if (vrrp_hdr->count_ip > 0) { uint32_t* virtual_ip = (uint32_t*)(buffer + ip_hdr_len + sizeof(vrrp_header)); char ip_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, virtual_ip, ip_str, sizeof(ip_str)); printf("Virtual IP: %s\n", ip_str); } close(sockfd); return 0; }
额外注意事项
- 运行原始套接字程序需要root权限(用
sudo执行)。 - 原始套接字会接收所有IP协议号为112的数据包,若需过滤可自行判断目标IP是否为224.0.0.18。
- 不同系统的原始套接字行为略有差异,Linux下需确保内核允许创建原始套接字。
内容的提问来源于stack exchange,提问作者Matthew
相关产品推荐
相关产品推荐

