systemd networkd 和/或解决了虚拟网络接口上接收(原始)数据包的阻塞问题?

systemd networkd 和/或解决了虚拟网络接口上接收(原始)数据包的阻塞问题?

在处理一些基本上将原始以太网数据包从一个 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

相关内容