在处理一些基本上将原始以太网数据包从一个 MACVLAN 发送到另一个 MACVLAN(虚拟)网络接口的单元测试代码时,我注意到大多数时候测试代码无法接收从第一个 MACVLAN 发送到第二个 MACVLAN 的任何数据包。使用 Wireshark,我可以看到数据包离开第一个 MACVLAN,但永远不会到达第二个 MACVLAN 或侦听原始套接字。只有在少数奇怪的情况下,任何数据包才会通过——测试代码没有任何变化。
主机系统是 Ubuntu 22.10(内核 5.19.0-38-generic)系统和网络管理员。
直到一段时间后,systemd-resolved、systemd-networkd 和网络管理器才引起了我的怀疑。通过在其自己的隔离瞬态网络命名空间中运行测试,我可以成功地确定,在这些主机服务无法访问的范围内,测试始终能够正确成功。
怀疑网络管理员——即使nmcli device status
告诉我虚拟虚拟和 MACVLAN 接口是“不受管理的——我发现https://developer-old.gnome.org/NetworkManager/stable/NetworkManager.conf.html然后为非托管设备添加通配符:
[keyfile]
unmanaged-devices=interface-name:docker*;interface-name:br-*;interface-name:veth*;interface-name:mcvl-*;interface-name:dumy-*
不幸的是,这并没有改善情况,并且几乎每次运行测试仍然会失败(即使多次重新启动网络管理器并确保配置文件正确后)。
在 Wireshark 中,我注意到 MDNS 在 MACVLAN 网络接口上广播,而这些接口本不应该有广播。我该如何告诉 systemd 的 networkd 和 solved 不让它们的脏爪子触及任何虚拟网络接口,尤其是 dummy、MACVLAN 和 VETH 网络接口?我搜索了配置选项,但找不到任何合适的选项。
知道如何让 systemd 的组件远离他们一开始就不应该接触的东西吗?
以下是基于 Ginkgo/Gomega 的单元测试,重现了这种情况。
package pingpong
import (
"bytes"
"context"
"fmt"
"net"
"os"
"strings"
"time"
"github.com/mdlayher/ethernet"
"github.com/mdlayher/packet"
"github.com/thediveo/notwork/dummy"
"github.com/thediveo/notwork/link"
"github.com/thediveo/notwork/macvlan"
"github.com/thediveo/notwork/netns"
"github.com/vishvananda/netlink"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/thediveo/success"
)
func TestPingPong(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "pingpong package")
}
const (
experimentalEthType = 0xffee // something (hopefully) unused
pings = 10
pingInterval = 100 * time.Millisecond
)
var payload = bytes.Repeat([]byte("HELO"), 100)
var _ = Describe("pingponging netdevs", Ordered, func() {
BeforeAll(func() {
if os.Geteuid() != 0 {
Skip("needs root")
}
})
DescribeTable("virtual network pingpong",
func(ctx context.Context, dropall bool) {
// By("creating a new network namespace")
// defer netns.EnterTransientNetns()()
By("creating two MACVLANs connected via a dummy network interface")
dummy := dummy.NewTransientUp()
macvlan1 := macvlan.NewTransient(dummy)
netlink.LinkSetUp(macvlan1)
macvlan2 := macvlan.NewTransient(dummy)
netlink.LinkSetUp(macvlan2)
macvlan1 = Successful(netlink.LinkByIndex(macvlan1.Attrs().Index))
mac1 := macvlan1.Attrs().HardwareAddr
macvlan2 = Successful(netlink.LinkByIndex(macvlan2.Attrs().Index))
mac2 := macvlan2.Attrs().HardwareAddr
Expect(mac1).NotTo(Equal(mac2))
By(fmt.Sprintf("waiting for MACVLANs (%s-%s, %s-%s) to become operationally UP",
macvlan1.Attrs().Name, macvlan1.Attrs().HardwareAddr.String(),
macvlan2.Attrs().Name, macvlan2.Attrs().HardwareAddr.String()))
link.EnsureUp(macvlan1)
link.EnsureUp(macvlan2)
By("opening data-link layer sockets")
txconn := Successful(packet.Listen(
&net.Interface{Index: macvlan1.Attrs().Index}, packet.Raw, experimentalEthType, nil))
defer txconn.Close()
rxconn := Successful(packet.Listen(
&net.Interface{Index: macvlan2.Attrs().Index}, packet.Raw, experimentalEthType, nil))
defer rxconn.Close()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
By("sending data-link layer PDUs")
go func() {
defer cancel()
defer GinkgoRecover()
f := ethernet.Frame{
Destination: mac2,
Source: mac1,
EtherType: experimentalEthType,
Payload: payload,
}
frame := Successful(f.MarshalBinary())
toAddr := packet.Addr{HardwareAddr: mac2}
for i := 0; i < pings; i++ {
By("sending something...")
_, err := txconn.WriteTo(frame, &toAddr)
Expect(err).NotTo(HaveOccurred())
select {
case <-ctx.Done():
return
case <-time.After(pingInterval):
}
}
}()
By("receiving data-link layer PDUs (or not)")
received := 0
receive:
for {
buffer := make([]byte, 1500)
rxconn.SetReadDeadline(time.Now().Add(1 * time.Second))
n, fromAddr, err := rxconn.ReadFrom(buffer)
select {
case <-ctx.Done():
break receive
default:
}
if err != nil && dropall && strings.Contains(err.Error(), "i/o timeout") {
continue
}
Expect(err).NotTo(HaveOccurred())
By("...received something")
f := ethernet.Frame{}
Expect(f.UnmarshalBinary(buffer[:n])).To(Succeed())
Expect(f.EtherType).To(Equal(ethernet.EtherType(experimentalEthType)))
Expect(fromAddr.(*packet.Addr).HardwareAddr).To(Equal(mac1))
Expect(f.EtherType).To(Equal(ethernet.EtherType(experimentalEthType)))
Expect(len(f.Payload)).To(BeNumerically(">=", len(payload)))
Expect(f.Payload[:len(payload)]).To(Equal(payload))
received++
}
if !dropall {
Expect(received).To(BeNumerically(">=", (2*pings)/3), "too much packet loss")
} else {
Expect(received).To(BeZero())
}
},
Entry("receives passed-on packets", false),
)
})
答案1
我花了一些时间才得到正确的灵感,也许有关于 MAC 地址的问题,所以我添加了这个安全期望,然后立即触发:
mac2 := Successful(netlink.LinkByIndex(macvlan2.Attrs().Index)).
Attrs().HardwareAddr
// ... something going on here
mac2now := Successful(netlink.LinkByIndex(macvlan2.Attrs().Index)).
Attrs().HardwareAddr
Expect(mac2now).To(Equal(mac2))
特别是查看一些*.link
配置,特别/usr/lib/systemd/network/
是有一个包罗万象的默认配置,99-default.link
如下所示:
[Match]
OriginalName=*
[Link]
NamePolicy=keep kernel database onboard slot path
AlternativeNamesPolicy=database onboard slot path
MACAddressPolicy=persistent
那么MACAddressPolicy=persistent
实际上是做什么的呢?这网络链接文档解释:
如果硬件有一个持久的 MAC 地址(大多数硬件都应该有),并且如果它被内核使用,则不会执行任何操作。否则,将生成一个新的 MAC 地址,该地址保证在给定机器和给定设备的每次启动时都相同,但在其他情况下是随机的。
因此,networkd
(或者实际上是这样udevd
?)将任何新 MACVLAN netdev 的原始 MAC 地址替换为另一个地址。
由于从 MACVLAN 出现开始需要一些时间,因此单元测试已查询原始 MAC 地址,并且当测试开始发送以太网数据包时,第二个 MACVLAN 的 MAC 地址已更改,因此原始目标 MAC 已随测试时不知道这一点。
修复方法是至少有选择地恢复到MACAddressPolicy=
,none
例如:
# /etc/systemd/network/00-notwork.link
[Match]
Kind=macvlan
OriginalName=mcvl-*
[Link]
Description="keep systemd's sticky fingers off test netdevs"
MACAddressPolicy=none