-
基本概念
-
混杂模式:
Promiscuous mode,是电脑网络中的术语。是指一台机器的网卡能够接收所有经过它的数据流,而不论其目的地址是否是它。
一般计算机网卡都工作在非混杂模式下,此时网卡只接受来自网络端口的目的地址指向自己的数据。当网卡工作在混杂模式下时,网卡将来自接口的所有数据都捕获并交给相应的驱动程序。
-
单播:
Unicast,指数据包在计算机网络的传输中,目的地址为单一目标的一种传输方式。 通常所使用的网络协议大多采用单播传输,例如TCP和UDP。
其特点是每次只有两个实体相互通信,发送端和接收端都是唯一确定的。
在IPv4网络中,0.0.0.0 到 223.255.255.255 属于单播地址。
-
多播(组播):
Multicast,也译作群播,是计算机网络中的一种群组通信,它把信息同时传递给一组目的计算机。多播可以是一对多或多对多布置。不应将其与物理层的点到多点通信混淆。
群组通信可由应用层多播实现,也可由网络级多播协助实现,后者能让一个源地址用一次传输将数据发给群组。数据到达包含该组成员的网络区域时,由路由器、交换机、基站子系统等网络组件自动完成复制分发。网络级多播可能通过数据链路层的一对多地址交换实现,如以太网多播地址、异步传输模式(ATM)、P2MP及Infiniband多播,也可能通过网络层由IP多播实现。在IP多播中,多播发生在IP路由层面,路由器创建一个最佳路径将数据发往多播目的地址。
多播通常应用于IP网络上的流媒体传输,如IPTV、多点视频会议(Multipoint videoconferencing) 等。
在 IPV4 网络中,224.0.0.0 到 239.255.255.255 属于多播地址。
-
广播:
Broadcast,是指将信息数据包发往指定网络范围内的所有设备[1]。其发送范围称为“广播域”。
并非所有的计算机网络都支持广播,例如X.25网络和帧中继都不支持广播,而且也没有在“整个互联网范围中”的广播。IPv6亦不支持广播,广播的相应功能由多播代替。
通常来说,广播都是限制在局域网范围内,比如以太网或令牌环网络。因为广播在广域网中可能造成比在局域网中大的多的影响。
在 IPV4 网络中,1 个局域网内,广播地址是网络号保持不变,主机号全为 1 的地址。如家庭局域网中网络地址 192.168.0.0,子网掩码 255.255.255.0,它的广播地址就是 192.168.0.255。公司局域网中网络地址 10.13.0.0,子网掩码 255.255.0.0,它的广播地址就是 10.13.255.255。
广播地址计算公式:网络地址 || (~子网掩码) = 广播地址
-
组播过滤
接下来以 2k1000la 使用的 stmac dwmac1000 GMAC 驱动为例,分析其组播过滤模式的具体流程。
涉及到的具体代码:
过滤模式设置部分代码:
static void dwmac1000_set_mchash(void __iomem *ioaddr, u32 *mcfilterbits,
int mcbitslog2)
{
int numhashregs, regs;
switch (mcbitslog2) {
case 6:
writel(mcfilterbits[0], ioaddr + GMAC_HASH_LOW);
writel(mcfilterbits[1], ioaddr + GMAC_HASH_HIGH);
return;
break;
case 7:
numhashregs = 4;
break;
case 8:
numhashregs = 8;
break;
default:
pr_debug("STMMAC: err in setting multicast filter\n");
return;
break;
}
for (regs = 0; regs < numhashregs; regs++)
writel(mcfilterbits[regs], ioaddr + GMAC_EXTHASH_BASE + regs * 4);
}
static void dwmac1000_set_filter(struct mac_device_info *hw,
struct net_device *dev)
{
void __iomem *ioaddr = (void __iomem *)dev->base_addr;
unsigned int value = 0;
#ifndef SYLIXOS
unsigned int perfect_addr_number = hw->unicast_filter_entries;
#endif
u32 mc_filter[8];
int mcbitslog2 = hw->mcast_bits_log2;
pr_debug("%s: # mcasts %d\n", __func__,
netdev_macfilter_count((netdev_t *)dev));
memset(mc_filter, 0, sizeof(mc_filter));
if (hw->flags & IFF_PROMISC) {
value = GMAC_FRAME_FILTER_PR | GMAC_FRAME_FILTER_PCF;
} else if (hw->flags & IFF_ALLMULTI) {
value = GMAC_FRAME_FILTER_PM; /* pass all multi */
} else if (!netdev_macfilter_isempty((netdev_t *)dev) && (mcbitslog2 == 0)) {
/* Fall back to all multicast if we've no filter */
value = GMAC_FRAME_FILTER_PM;
} else if (!netdev_macfilter_isempty((netdev_t *)dev)) {
struct netdev_mac *ha;
/* Hash filter for multicast */
value = GMAC_FRAME_FILTER_HMC;
NETDEV_MACFILTER_FOREACH((netdev_t *)dev, ha) {
/* The upper n bits of the calculated CRC are used to
* index the contents of the hash table. The number of
* bits used depends on the hardware configuration
* selected at core configuration time.
*/
int bit_nr = bitrev32(~crc32_le(~0, ha->hwaddr,
ETH_ALEN)) >>
(32 - mcbitslog2);
/* The most significant bit determines the register to
* use (H/L) while the other 5 bits determine the bit
* within the register.
*/
mc_filter[bit_nr >> 5] |= 1 << (bit_nr & 31);
}
}
value |= GMAC_FRAME_FILTER_HPF;
dwmac1000_set_mchash(ioaddr, mc_filter, mcbitslog2);
#ifndef SYLIXOS
/* Handle multiple unicast addresses (perfect filtering) */
if (netdev_uc_count(dev) > perfect_addr_number)
/* Switch to promiscuous mode if more than unicast
* addresses are requested than supported by hardware.
*/
value |= GMAC_FRAME_FILTER_PR;
else {
int reg = 1;
struct netdev_hw_addr *ha;
netdev_for_each_uc_addr(ha, dev) {
stmmac_set_mac_addr(ioaddr, ha->addr,
GMAC_ADDR_HIGH(reg),
GMAC_ADDR_LOW(reg));
reg++;
}
while (reg < perfect_addr_number) {
writel(0, ioaddr + GMAC_ADDR_HIGH(reg));
writel(0, ioaddr + GMAC_ADDR_LOW(reg));
reg++;
}
}
#endif
#ifdef FRAME_FILTER_DEBUG
/* Enable Receive all mode (to debug filtering_fail errors) */
value |= GMAC_FRAME_FILTER_RA;
#endif
writel(value, ioaddr + GMAC_FRAME_FILTER);
}
此段代码逻辑:
涉及到的 GMAC 寄存器:GMAC_FRAME_FILTER(帧过滤寄存器)、GMAC_HASH_LOW/HIGH(低/高哈希寄存器)、GMAC_EXT_HASH_BASE(拓展哈希寄存器)
GMAC_FRAME_FILTER 常用位(该寄存器的位 30:11 是保留位,没有列出):
宏 | 简介 |
---|---|
GMAC_FRAME_FILTER_PR | 混杂模式使能,置 1 开启混杂模式 |
GMAC_FRAME_FILTER_HUC | 哈希单播过滤 |
GMAC_FRAME_FILTER_HMC | 哈希组播过滤 |
GMAC_FRAME_FILTER_DAIF | DA(目的地址)反过滤使能 |
GMAC_FRAME_FILTER_PM | 通过所有组播使能 |
GMAC_FRAME_FILTER_DBF | 禁止广播使能 |
GMAC_FRAME_FILTER_PCF | 通过控制帧模式设置 |
GMAC_FRAME_FILTER_SAIF | SA(源地址)反过滤使能 |
GMAC_FRAME_FILTER_SAF | SA(源地址)过滤使能 |
GMAC_FRAME_FILTER_HPF | 哈希/完美哈希过滤使能 |
GMAC_FRAME_FILTER_RA | 接收所有帧并传递上层使能 |
GMAC_HASH_LOW/HIGH:这两个寄存器均为 32 位,组成一个 64 位的哈希表。在对帧的 DA 地址进行过滤时,对这个 DA 地址进行 CRC 校验(运算由硬件完成)并产生一个 6 位的校验值(范围 0~63),该校验值作为哈希表的索引,如校验值为 0x2C(十进制 44),就会去查找哈希表的 44 位,也就是 GMAC_HASH_HIGH 的 12 位。如果该位为 1,通过,为 0,不通过。根据代码逻辑,这个哈希表仅在设置宏 HASH_TABLE_SIZE 为 64 时使用。
GMAC_EXT_HASH_BASE:如果 HASH_TABLE_SIZE 大于 64,使用这个寄存器(从此偏移开始的寄存器)作为哈希表。
更多详情可以参考 Synopsys ethernet book 282-284 页。
涉及到的内核标志位(posix if flags):
宏 | 简介 |
---|---|
IFF_UP | 网卡启用 |
IFF_BOARDCAST | 网卡广播 |
IFF_POINTTOPOINT | 网卡 P2P 模式 |
IFF_RUNNING | 网卡运行中 |
IFF_MULTICAST | 网卡支持组播 |
IFF_LOOPBACK | 网卡回环 |
IFF_NOARP | 网卡不使用 ARP 协议 |
IFF_PROMISC | 网卡混杂模式 |
IFF_ALLMULTI | 网卡接收所有组播 |
代码逻辑部分:
-
如果网卡开启混杂模式,设置帧过滤寄存器相应标志位并写入(虽然其后调用了 set_mchash 设置哈希表,但没有作用)
-
不符合 1,如果网卡开启接收所有组播,设置帧过滤寄存器通过所有组播标志位并写入(同上)
-
不符合 1、2,如果网卡设置的 mac 过滤器为空且设置的哈希表大小为 0,设置帧过滤寄存器通过所有组播标志位并写入(同上)
-
不符合 1、2、3,如果网卡设置的 mac 过滤器不为空,设置帧过滤寄存器哈希组播过滤标志位并在之后写入,根据已设置的 mac 过滤器的 mac 地址计算哈希表内容,然后写入相应寄存器,最后设置帧过滤寄存器哈希/完美哈希过滤标志位并在之后写入。
代码中的部分变量/常量含义解释:
-
HASH_TABLE_SIZE:
mac 地址过滤用哈希表的长度。
-
mc_filter:
用来临时存放哈希表,长度 32 * 8 = 256 位。
-
mcbitslog2:
这个值由使用的哈希表长度,也就是宏 HASH_TABLE_SIZE 决定,其值等于 log(2,HASH_TABLE_SIZE),此值决定了哈希表的最终存放位置是 GMAC_HASH_LOW/HIGH 还是 GMAC_EXT_HASH_BASE(从此偏移开始)。
-
numhashregs:
由 mcbitslog2 决定,其值为 HASH_TABLE_SIZE / 32,也就是用到的 GMAC_EXT_HASH_BASE 开始的寄存器的个数。
-
bit_nr:
计算哈希表用到的中间变量。
-
hw->hwaddr:
计算哈希表用到的参数,2k1000la 中用到了 3 个,其来源于内核。
其中,
01:00:5e:00:00:01 是 IPV4组播地址 224.0.0.1 的组播 MAC 地址
33:33:ff:11:22:22 是 IPV6 组播 ff:02::01:ff:11:22:22 的组播 MAC 地址。
33:33:00:00:00:01 是 IPV6 组播 ff:02::01 的组播 MAC 地址。
这 3 个地址是内核中 LWIP 协议栈计算出来的并传递到驱动的。
涉及到内核 LWIP 协议栈中的两个函数 netdev_netif_igmp_mac_filter
和 netdev_netif_mld_mac_filter
,这里不进行详细讲解。
具体计算过程
int
bit_nr
=
bitrev32
(~
crc32_le
(~0,
ha
->
hwaddr, ETH_ALEN
)) >> (32 -
mcbitslog2
);
ha->hwaddr 是上文提到的 3 个地址, ETH_ALEN 是 MAC 地址长度,为 6,mcbitslog2 是规定的 HASH_TABLE_SIZE(64) 取 2 的对数,值为 6。
bitrev32 宏: 翻转 u32 类型参数的位顺序,举例,bitrev32(0x00010001)=0x80008000。
具体实现如下:
宏版(首次交换高16位和低16位,第二次以8位为间隔交换,以此类推):
函数版(__bitrev8 使用查表法计算,__bitrev16 以8位为间隔交换然后调用 __bitrev8,以此类推):
crc32_le 是 crc32 反向校验函数,其具体实现如下:
根据以上条件,可以得出下面的结论,此结论同时也是组播过滤的具体流程。
驱动中设置组播过滤模式时,会设置帧过滤寄存器的哈希组播过滤标志位,会利用内核传递来的组播 mac 地址(就是上文提到的 3 个地址)计算出一张 64/128/256 位(位数取决于 HASH_TABLE_SIZE)的哈希表。根据这张哈希表来过滤帧。不考虑哈希碰撞,简单理解,这种过滤模式就是一种白名单制度,由内核传入的 mac 地址就是白名单成员,要过滤的帧只有其 DA 地址在白名单中方能通过过滤传递到上层。
证明这一点的方法如下:
修改 bit_nr 计算流程如下,手动设置白名单(为了简单起见,这里只设置了一个,这个 mac 地址对应的 ip 是 224.0.0.2)
在虚拟机 linux 系统(虚拟机必须使用桥接网卡的模式且开启网卡混杂模式)中使用 ping -b 224.0.0.2 命令进行验证,发现 2k1000la 果然收到 DA 为 224.0.0.2 的帧。修改 tempHwaddr 并多次尝试,可以证明这一点。
224.0.0.2 | 224.0.0.4 |
---|---|
继续深入内核相关代码,发现 224.0.0.1 在此处被设置。
-
QA
为什么 ls2k1000la 修改 HASH_TABLE_SIZE 为 64 就不再接收到组播了,按理说是应该会收到 DA 为 224.0.0.1 的组播?
正确的说,ls2k1000la 的情况属于误打误撞,是错误的修改,只是这种错误恰好符合了测试不能收到组播的新规定。
因为 224.0.0.1 是被加到白名单中的。当 HASH_TABLE_SIZE = 64 时,驱动将计算好的哈希表写入到 GMAC_HASH_LOW/HIGH 这两个寄存器中,然而 ls2k1000la 中这两个寄存器的写入是无效的(linux 下也一样,并且调用命令手动写入 0xFFFFFFF 也是无效的,由于一开始只关注到了 linux 和 SylixOS 下的代码流程差异,忽略了这一点,错误地认为这是正常现象),也就是说这个过滤用的白名单实际上是空的并且永远是空的。
总结,虽然修改 HASH_TABLE_SIZE 为 64 既符合官方 linux 代码又达到了测试的新规定,但这种修改是表象,代码没有真正地发挥作用,反而会对网卡组播功能的正常使用产生影响。
为什么 3568 会收到多得多的组播包,例如 DA 为 224.0.0.1、224.0.1.129、224.0.0.252 等?
这是由于哈希冲突引起的, 3568 使用的是 64 位的哈希表。
也就是说任意一个 DA,其经过 CRC 校验后生成的 bit_nr 为 8、32、1,就可以通过过滤器。也就说只要验证这些 IP 的组播 MAC 地址经 CRC 校验生成的 bit_nr 就好了,为了便于计算,添加以下命令。
static void __stmmac_hash_calculate (unsigned char const *hwaddr, u32 mcbitslog2)
{
u32 bit_nr = bitrev32(~crc32_le(~0, hwaddr, 6)) >> (32 - mcbitslog2);
printk("hwaddr = %02x:%02x:%02x:%02x:%02x:%02x\r\n",
hwaddr[0], hwaddr[1], hwaddr[2], hwaddr[3], hwaddr[4], hwaddr[5]);
printk("mcbitslog2 = %u\r\n", mcbitslog2);
printk("bit_nr = %u\r\n", bit_nr);
printk("mc_filter[%u] |= 0x%08x\r\n", bit_nr >> 5, (1 << (bit_nr & 0x1f)));
}
static int stmmac_hash_calculate (int argc, char *argv[])
{
u8 hwaddr[8] = { 0 };
u32 mcbitslog2 = 0;
printk("hwaddr = %s\r\n", argv[1]);
printk("mcbitslog2 = %s\r\n", argv[2]);
sscanf(argv[1], "%02x:%02x:%02x:%02x:%02x:%02x", &(hwaddr[0]), &(hwaddr[1]), &(hwaddr[2]), &(hwaddr[3]), &(hwaddr[4]), &(hwaddr[5]));
sscanf(argv[2], "%u", &mcbitslog2);
__stmmac_hash_calculate(hwaddr, mcbitslog2);
return 0;
}
void stmmac_info_dump_init (struct stmmac_priv *priv, const int num)
{
stmmac_dump_priv[num] = priv;
API_TShellKeywordAdd("dwdump", stmmac_info_dump);
API_TShellFormatAdd("dwdump", "[index] [hostreg | dmareg | txdes | rxdes | all]");
API_TShellHelpAdd("dwdump", "\n\rshow dw mac info.\n eg:dwdump 0 hostreg\n");
API_TShellKeywordAdd("dw_hash", stmmac_hash_calculate);
}
224.0.0.1/xxx.0.0.1,根据前面提到的计算方法,其低 23 位有效(x.0.0.1),其 IPV4 组播 MAC 地址为 01:00:5e:00:00:01,使用命令调用 CRC 运算,其 bit_nr = 32,可以通过。
224.0.1.129/xxx.0.1.129,其 IPV4 组播 MAC 地址为 01:00:5e:00:01:81,使用命令调用 CRC 运算,其 bit_nr = 1,可以通过。
224.0.0.252/xxx.0.0.252,其 IPV4 组播 MAC 地址为 01:00:5e:00:00:fc,使用命令调用 CRC 运算,其 bit_nr = 1,可以通过。
-
暂时性解决方案
由 ls2k1000la 和 rk3568 可以看出,原来的 GMAC 代码并没有太大的问题,为了符合测试部的新标准,可以采取以下方案暂时性的解决此问题(网络组后续会添加组播精准过滤相关代码到网卡驱动,逐步弃用容易产生哈希冲突的哈希过滤模式,因此此方法仅为暂时性的解决方案):
在网卡驱动中提供一个给测试部门测试用例使用的命令,形式如下:
[root@sylixos/root]#check_ipv4_mc 224.0.0.1
check success: 224.0.0.1 is mapped in the gmac hash table.
[root@sylixos/root]#check_ipv4_mc 224.0.0.12
check failed: 224.0.0.12 is not mapped in the gmac hash table.
测试部在测试此项功能时会调用此命令以获取一个不在哈希表中的组播 IP,并在局域网中 ping 此 IP,观察 af_packet 是否能收到 DA 地址为此 IP 的数据包。对于 af_packet 中收到的目的地址也可使用此命令进行相关验证。