diff --git a/pkg/abi/linux/netlink_route.go b/pkg/abi/linux/netlink_route.go index 345cbcde9..b3deba1e9 100644 --- a/pkg/abi/linux/netlink_route.go +++ b/pkg/abi/linux/netlink_route.go @@ -168,6 +168,21 @@ const ( IFLA_GSO_MAX_SIZE = 41 ) +// Interface link info attributes, from uapi/linux/if_link.h. +const ( + IFLA_INFO_UNSPEC = 0 + IFLA_INFO_KIND = 1 + IFLA_INFO_DATA = 2 + IFLA_INFO_XSTATS = 3 + IFLA_INFO_SLAVE_KIND = 4 + IFLA_INFO_SLAVE_DATA = 5 +) + +// Virtuall ethernet attributes, from uapi/linux/veth.h. +const ( + VETH_INFO_PEER = 1 +) + // InterfaceAddrMessage is struct ifaddrmsg, from uapi/linux/if_addr.h. // // +marshal diff --git a/pkg/sentry/inet/context.go b/pkg/sentry/inet/context.go index e8cc1bffd..bf16510f2 100644 --- a/pkg/sentry/inet/context.go +++ b/pkg/sentry/inet/context.go @@ -24,6 +24,8 @@ type contextID int const ( // CtxStack is a Context.Value key for a network stack. CtxStack contextID = iota + // CtxNamespaceByFD is a Context.Value key for NamespaceByFD. + CtxNamespaceByFD ) // StackFromContext returns the network stack associated with ctx. @@ -33,3 +35,16 @@ func StackFromContext(ctx context.Context) Stack { } return nil } + +// NamespaceByFD returns the network namespace associated with the specified +// file descriptor. +type NamespaceByFD = func(fd int32) (*Namespace, error) + +// NamespaceByFDFromContext returns NamespaceByFD to lookup the network +// namespace associated with the specified file descriptor. +func NamespaceByFDFromContext(ctx context.Context) NamespaceByFD { + if v := ctx.Value(CtxNamespaceByFD); v != nil { + return v.(NamespaceByFD) + } + return nil +} diff --git a/pkg/sentry/inet/inet.go b/pkg/sentry/inet/inet.go index 2a73abdb8..c1a5d668b 100644 --- a/pkg/sentry/inet/inet.go +++ b/pkg/sentry/inet/inet.go @@ -247,3 +247,25 @@ const ( TCP_RACK_STATIC_REO_WND TCP_RACK_NO_DUPTHRESH ) + +// InterfaceRequest contains information about an adding interface. +type InterfaceRequest struct { + // Kind is the link type. + Kind string + // Name is the interface name. + Name string + // Addr is the hardware device address. + Addr []byte + // MTU is the maximum transmission unit. + MTU uint32 + // Data is link type specific device properties. + Data any +} + +// VethPeerReq contains information about a second interface of a new veth pair. +type VethPeerReq struct { + // Req is information about the second end of the new veth pair. + Req InterfaceRequest + // Stack is the stack where the second end has to be added. + Stack Stack +} diff --git a/pkg/sentry/kernel/task_context.go b/pkg/sentry/kernel/task_context.go index 6b7b9a98f..5206a269d 100644 --- a/pkg/sentry/kernel/task_context.go +++ b/pkg/sentry/kernel/task_context.go @@ -108,6 +108,8 @@ func (t *Task) contextValue(key any, isTaskGoroutine bool) any { return t.k.GetDevGoferClient(t.k.ContainerName(t.containerID)) case inet.CtxStack: return t.NetworkContext() + case inet.CtxNamespaceByFD: + return t.NetworkNamespaceByFD case ktime.CtxRealtimeClock: return t.k.RealtimeClock() case limits.CtxLimits: diff --git a/pkg/sentry/kernel/task_net.go b/pkg/sentry/kernel/task_net.go index e448bb18b..0da2c88a9 100644 --- a/pkg/sentry/kernel/task_net.go +++ b/pkg/sentry/kernel/task_net.go @@ -15,6 +15,9 @@ package kernel import ( + "gvisor.dev/gvisor/pkg/errors/linuxerr" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/kernfs" + "gvisor.dev/gvisor/pkg/sentry/fsimpl/nsfs" "gvisor.dev/gvisor/pkg/sentry/inet" ) @@ -50,3 +53,27 @@ func (t *Task) GetNetworkNamespace() *inet.Namespace { t.mu.Unlock() return netns } + +// NetworkNamespaceByFD returns the network namespace associated with the specified descriptor. +func (t *Task) NetworkNamespaceByFD(fd int32) (*inet.Namespace, error) { + file := t.GetFile(fd) + if file == nil { + return nil, linuxerr.EBADF + } + defer file.DecRef(t) + + d, ok := file.Dentry().Impl().(*kernfs.Dentry) + if !ok { + return nil, linuxerr.EINVAL + } + i, ok := d.Inode().(*nsfs.Inode) + if !ok { + return nil, linuxerr.EINVAL + } + ns, ok := i.Namespace().(*inet.Namespace) + if !ok { + return nil, linuxerr.EINVAL + } + ns.IncRef() + return ns, nil +} diff --git a/pkg/sentry/socket/netstack/BUILD b/pkg/sentry/socket/netstack/BUILD index 511e09e9b..61d2d08ca 100644 --- a/pkg/sentry/socket/netstack/BUILD +++ b/pkg/sentry/socket/netstack/BUILD @@ -45,7 +45,10 @@ go_library( "//pkg/syserr", "//pkg/tcpip", "//pkg/tcpip/header", + "//pkg/tcpip/link/ethernet", + "//pkg/tcpip/link/packetsocket", "//pkg/tcpip/link/tun", + "//pkg/tcpip/link/veth", "//pkg/tcpip/network/ipv4", "//pkg/tcpip/network/ipv6", "//pkg/tcpip/stack", diff --git a/pkg/sentry/socket/netstack/stack.go b/pkg/sentry/socket/netstack/stack.go index 2dcb1be39..3a1ef8e51 100644 --- a/pkg/sentry/socket/netstack/stack.go +++ b/pkg/sentry/socket/netstack/stack.go @@ -27,6 +27,9 @@ import ( "gvisor.dev/gvisor/pkg/syserr" "gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/link/ethernet" + "gvisor.dev/gvisor/pkg/tcpip/link/packetsocket" + "gvisor.dev/gvisor/pkg/tcpip/link/veth" "gvisor.dev/gvisor/pkg/tcpip/network/ipv4" "gvisor.dev/gvisor/pkg/tcpip/network/ipv6" "gvisor.dev/gvisor/pkg/tcpip/stack" @@ -104,18 +107,18 @@ func (s *Stack) RemoveInterface(idx int32) error { // SetInterface implements inet.Stack.SetInterface. func (s *Stack) SetInterface(ctx context.Context, msg *nlmsg.Message) *syserr.Error { var ifinfomsg linux.InterfaceInfoMessage - attrs, ok := msg.GetData(&ifinfomsg) + attrsView, ok := msg.GetData(&ifinfomsg) if !ok { return syserr.ErrInvalidArgument } - for !attrs.Empty() { - // The index is unspecified, search by the interface name. - ahdr, value, rest, ok := attrs.ParseFirst() - if !ok { - return syserr.ErrInvalidArgument - } - attrs = rest - switch ahdr.Type { + attrs, ok := attrsView.Parse() + if !ok { + return syserr.ErrInvalidArgument + } + ifname := "" + for attr := range attrs { + value := attrs[attr] + switch attr { case linux.IFLA_IFNAME: if len(value) < 1 { return syserr.ErrInvalidArgument @@ -124,23 +127,28 @@ func (s *Stack) SetInterface(ctx context.Context, msg *nlmsg.Message) *syserr.Er // Device name changing isn't supported yet. return syserr.ErrNotSupported } - ifname := string(value[:len(value)-1]) + ifname = value.String() for idx, ifa := range s.Interfaces() { if ifname == ifa.Name { ifinfomsg.Index = idx break } } + case linux.IFLA_MASTER: + case linux.IFLA_LINKINFO: default: - ctx.Warningf("unexpected attribute: %x", ahdr.Type) + ctx.Warningf("unexpected attribute: %x", attr) return syserr.ErrNotSupported } } + flags := msg.Header().Flags if ifinfomsg.Index == 0 { + if flags&linux.NLM_F_CREATE != 0 { + return s.newInterface(ctx, msg, attrs) + } return syserr.ErrNoDevice } - flags := msg.Header().Flags if flags&(linux.NLM_F_EXCL|linux.NLM_F_REPLACE) != 0 { return syserr.ErrExists } @@ -156,9 +164,123 @@ func (s *Stack) SetInterface(ctx context.Context, msg *nlmsg.Message) *syserr.Er } // Netstack interfaces are always up. } + return nil } +const defaultMTU = 1500 + +func (s *Stack) newVeth(ctx context.Context, linkAttrs map[uint16]nlmsg.BytesView, linkInfoAttrs map[uint16]nlmsg.BytesView) *syserr.Error { + var ( + linkInfoData map[uint16]nlmsg.BytesView + ifinfomsg linux.InterfaceInfoMessage + peerLinkAttrs map[uint16]nlmsg.BytesView + ) + + peerStack := s + peerName := "" + ifname := "" + + if v, ok := linkAttrs[linux.IFLA_IFNAME]; ok { + ifname = v.String() + } + if value, ok := linkInfoAttrs[linux.IFLA_INFO_DATA]; ok { + linkInfoData, ok = nlmsg.AttrsView(value).Parse() + if !ok { + return syserr.ErrInvalidArgument + } + if v, ok := linkInfoData[linux.VETH_INFO_PEER]; ok { + attrsView := nlmsg.AttrsView(v[ifinfomsg.SizeBytes():]) + if !ok { + return syserr.ErrInvalidArgument + } + peerLinkAttrs, ok = attrsView.Parse() + if !ok { + return syserr.ErrInvalidArgument + } + if v, ok = peerLinkAttrs[linux.IFLA_IFNAME]; ok { + peerName = v.String() + } + if v, ok = peerLinkAttrs[linux.IFLA_NET_NS_FD]; ok { + fd, ok := v.Uint32() + if !ok { + return syserr.ErrInvalidArgument + } + f := inet.NamespaceByFDFromContext(ctx) + if f == nil { + return syserr.ErrInvalidArgument + } + ns, err := f(int32(fd)) + if err != nil { + return syserr.FromError(err) + } + defer ns.DecRef(ctx) + peerStack = ns.Stack().(*Stack) + } + } + } + ep, peerEP := veth.NewPair(defaultMTU) + id := tcpip.NICID(s.Stack.UniqueID()) + peerID := tcpip.NICID(peerStack.Stack.UniqueID()) + if ifname == "" { + ifname = fmt.Sprintf("veth%d", id) + } + err := s.Stack.CreateNICWithOptions(id, packetsocket.New(ethernet.New(ep)), stack.NICOptions{ + Name: ifname, + }) + if err != nil { + return syserr.TranslateNetstackError(err) + } + ep.SetStack(s.Stack, id) + + if peerName == "" { + peerName = fmt.Sprintf("veth%d", peerID) + } + err = peerStack.Stack.CreateNICWithOptions(peerID, packetsocket.New(ethernet.New(peerEP)), stack.NICOptions{ + Name: peerName, + }) + if err != nil { + peerEP.Close() + return syserr.TranslateNetstackError(err) + } + peerEP.SetStack(peerStack.Stack, id) + + return nil +} + +func (s *Stack) newInterface(ctx context.Context, msg *nlmsg.Message, linkAttrs map[uint16]nlmsg.BytesView) *syserr.Error { + var ( + linkInfoAttrs map[uint16]nlmsg.BytesView + kind string + ) + + if v, ok := linkAttrs[linux.IFLA_LINKINFO]; ok { + linkInfoAttrs, ok = nlmsg.AttrsView(v).Parse() + if !ok { + return syserr.ErrInvalidArgument + } + + for attr := range linkInfoAttrs { + value := linkInfoAttrs[attr] + switch attr { + case linux.IFLA_INFO_KIND: + kind = value.String() + case linux.IFLA_INFO_DATA: + default: + ctx.Warningf("unexpected link info attribute: %x", attr) + return syserr.ErrNotSupported + } + } + } + switch kind { + case "": + return syserr.ErrInvalidArgument + case "veth": + return s.newVeth(ctx, linkAttrs, linkInfoAttrs) + } + return syserr.ErrNotSupported +} + // InterfaceAddrs implements inet.Stack.InterfaceAddrs. func (s *Stack) InterfaceAddrs() map[int32][]inet.InterfaceAddr { nicAddrs := make(map[int32][]inet.InterfaceAddr) diff --git a/pkg/tcpip/link/veth/BUILD b/pkg/tcpip/link/veth/BUILD new file mode 100644 index 000000000..3fd5b1c3b --- /dev/null +++ b/pkg/tcpip/link/veth/BUILD @@ -0,0 +1,18 @@ +load("//tools:defs.bzl", "go_library") + +package( + default_applicable_licenses = ["//:license"], + licenses = ["notice"], +) + +go_library( + name = "veth", + srcs = ["veth.go"], + visibility = ["//visibility:public"], + deps = [ + "//pkg/sync", + "//pkg/tcpip", + "//pkg/tcpip/header", + "//pkg/tcpip/stack", + ], +) diff --git a/pkg/tcpip/link/veth/veth.go b/pkg/tcpip/link/veth/veth.go new file mode 100644 index 000000000..885ca1d90 --- /dev/null +++ b/pkg/tcpip/link/veth/veth.go @@ -0,0 +1,207 @@ +// Copyright 2024 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package veth provides the implementation of virtual ethernet device pair. +package veth + +import ( + "gvisor.dev/gvisor/pkg/sync" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +var _ stack.LinkEndpoint = (*Endpoint)(nil) +var _ stack.GSOEndpoint = (*Endpoint)(nil) + +// +stateify savable +type vethPacket struct { + e *Endpoint + protocol tcpip.NetworkProtocolNumber + pkt *stack.PacketBuffer +} + +const backlogQueueSize = 64 + +// Endpoint is link layer endpoint that redirects packets to a pair veth endpoint. +// +// +stateify savable +type Endpoint struct { + pair *Endpoint + mtu uint32 + + backlogQueue *chan vethPacket + + // linkAddr is the local address of this endpoint. + // linkaddr is immutable. + linkAddr tcpip.LinkAddress + + mu sync.RWMutex `state:"nosave"` + // +checklocks:mu + dispatcher stack.NetworkDispatcher + + // +checklocks:mu + stack *stack.Stack + // +checklocks:mu + idx tcpip.NICID +} + +// NewPair creates a new veth pair. +func NewPair(mtu uint32) (*Endpoint, *Endpoint) { + backlogQueue := make(chan vethPacket, backlogQueueSize) + a := &Endpoint{ + mtu: mtu, + linkAddr: tcpip.GetRandMacAddr(), + backlogQueue: &backlogQueue, + } + b := &Endpoint{ + mtu: mtu, + pair: a, + linkAddr: tcpip.GetRandMacAddr(), + backlogQueue: &backlogQueue, + } + a.pair = b + go func() { + for t := range backlogQueue { + t.e.InjectInbound(t.protocol, t.pkt) + t.pkt.DecRef() + } + }() + return a, b +} + +// SetStack stores the stack and the device index. +func (e *Endpoint) SetStack(s *stack.Stack, idx tcpip.NICID) { + e.mu.Lock() + defer e.mu.Unlock() + e.stack = s + e.idx = idx +} + +// Close closes e. Further packet injections will return an error, and all pending +// packets are discarded. Close may be called concurrently with WritePackets. +func (e *Endpoint) Close() { + e.mu.Lock() + stack := e.stack + e.stack = nil + e.mu.Unlock() + if stack == nil { + return + } + + e = e.pair + e.mu.Lock() + stack = e.stack + idx := e.idx + e.stack = nil + e.mu.Unlock() + if stack != nil { + stack.RemoveNIC(idx) + } + close(*e.backlogQueue) +} + +// InjectInbound injects an inbound packet. If the endpoint is not attached, the +// packet is not delivered. +func (e *Endpoint) InjectInbound(protocol tcpip.NetworkProtocolNumber, pkt *stack.PacketBuffer) { + e.mu.RLock() + d := e.dispatcher + e.mu.RUnlock() + if d != nil { + d.DeliverNetworkPacket(protocol, pkt) + } +} + +// Attach saves the stack network-layer dispatcher for use later when packets +// are injected. +func (e *Endpoint) Attach(dispatcher stack.NetworkDispatcher) { + e.mu.Lock() + defer e.mu.Unlock() + e.dispatcher = dispatcher +} + +// IsAttached implements stack.LinkEndpoint.IsAttached. +func (e *Endpoint) IsAttached() bool { + e.mu.RLock() + defer e.mu.RUnlock() + return e.dispatcher != nil +} + +// MTU implements stack.LinkEndpoint.MTU. It returns the value initialized +// during construction. +func (e *Endpoint) MTU() uint32 { + return e.mtu +} + +// Capabilities implements stack.LinkEndpoint.Capabilities. +func (e *Endpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload | stack.CapabilityTXChecksumOffload | stack.CapabilitySaveRestore +} + +// GSOMaxSize implements stack.GSOEndpoint. +func (*Endpoint) GSOMaxSize() uint32 { + return stack.GVisorGSOMaxSize +} + +// SupportedGSO implements stack.GSOEndpoint. +func (e *Endpoint) SupportedGSO() stack.SupportedGSO { + return stack.GVisorGSOSupported +} + +// MaxHeaderLength returns the maximum size of the link layer header. Given it +// doesn't have a header, it just returns 0. +func (*Endpoint) MaxHeaderLength() uint16 { + return 0 +} + +// LinkAddress returns the link address of this endpoint. +func (e *Endpoint) LinkAddress() tcpip.LinkAddress { + return e.linkAddr +} + +// WritePackets stores outbound packets into the channel. +// Multiple concurrent calls are permitted. +func (e *Endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) { + n := 0 + for _, pkt := range pkts.AsSlice() { + // In order to properly loop back to the inbound side we must create a + // fresh packet that only contains the underlying payload with no headers + // or struct fields set. + newPkt := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: pkt.ToBuffer(), + }) + (*e.backlogQueue) <- vethPacket{ + e: e.pair, + protocol: pkt.NetworkProtocolNumber, + pkt: newPkt, + } + n++ + } + + return n, nil +} + +// Wait implements stack.LinkEndpoint.Wait. +func (*Endpoint) Wait() {} + +// ARPHardwareType implements stack.LinkEndpoint.ARPHardwareType. +func (*Endpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +// AddHeader implements stack.LinkEndpoint.AddHeader. +func (e *Endpoint) AddHeader(pkt *stack.PacketBuffer) {} + +// ParseHeader implements stack.LinkEndpoint.ParseHeader. +func (e *Endpoint) ParseHeader(pkt *stack.PacketBuffer) bool { return true } diff --git a/pkg/tcpip/tcpip.go b/pkg/tcpip/tcpip.go index c4e3b576b..ec111d8cf 100644 --- a/pkg/tcpip/tcpip.go +++ b/pkg/tcpip/tcpip.go @@ -35,6 +35,8 @@ import ( "io" "math" "math/bits" + "math/rand" + "net" "reflect" "strconv" "strings" @@ -2710,6 +2712,15 @@ func ParseMACAddress(s string) (LinkAddress, error) { return LinkAddress(addr), nil } +// GetRandMacAddr returns a mac address that can be used for local virtual devices. +func GetRandMacAddr() LinkAddress { + mac := make(net.HardwareAddr, 6) + rand.Read(mac) // Fill with random data. + mac[0] &^= 0x1 // Clear multicast bit. + mac[0] |= 0x2 // Set local assignment bit (IEEE802). + return LinkAddress(mac) +} + // AddressWithPrefix is an address with its subnet prefix length. // // +stateify savable diff --git a/runsc/boot/network.go b/runsc/boot/network.go index 1315b1bbb..823193b52 100644 --- a/runsc/boot/network.go +++ b/runsc/boot/network.go @@ -255,7 +255,7 @@ func (n *Network) CreateLinksAndRoutes(args *CreateLinksAndRoutesArgs, _ *struct // Loopback normally appear before other interfaces. for _, link := range args.LoopbackLinks { - nicID++ + nicID = tcpip.NICID(n.Stack.UniqueID()) nicids[link.Name] = nicID linkEP := ethernet.New(loopback.New()) diff --git a/test/syscalls/linux/socket_netlink_route.cc b/test/syscalls/linux/socket_netlink_route.cc index 4bd48d7a0..044c05bb0 100644 --- a/test/syscalls/linux/socket_netlink_route.cc +++ b/test/syscalls/linux/socket_netlink_route.cc @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include #include @@ -1282,6 +1284,79 @@ TEST(NetlinkRouteTest, PasscredCreds) { EXPECT_THAT(creds.gid, AnyOf(Eq(0), Eq(65534))); } +#ifndef NLMSG_TAIL +#define NLMSG_TAIL(nmsg) \ + ((struct rtattr*)(((char*)(nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len))) +#endif + +void addattr(struct nlmsghdr* n, int maxlen, int type, const void* data, + int alen) { + int len = NLA_HDRLEN + alen; + struct rtattr* rta; + + ASSERT_LE(NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len), maxlen); + + rta = NLMSG_TAIL(n); + rta->rta_type = type; + rta->rta_len = len; + memcpy(RTA_DATA(rta), data, alen); + n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len); +} + +TEST(NetlinkRouteTest, VethAdd) { + SKIP_IF(!ASSERT_NO_ERRNO_AND_VALUE(HaveCapability(CAP_NET_ADMIN))); + SKIP_IF(IsRunningWithHostinet()); + + Link loopback_link = ASSERT_NO_ERRNO_AND_VALUE(LoopbackLink()); + + FileDescriptor fd = + ASSERT_NO_ERRNO_AND_VALUE(NetlinkBoundSocket(NETLINK_ROUTE)); + + struct request { + struct nlmsghdr hdr; + struct ifinfomsg ifm; + char buf[1024]; + }; + + struct request req = {}; + req.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg)); + req.hdr.nlmsg_type = RTM_NEWLINK; + req.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE; + req.hdr.nlmsg_seq = kSeq; + req.ifm.ifi_family = AF_UNSPEC; + req.ifm.ifi_index = 0; + req.ifm.ifi_change = IFF_UP; + req.ifm.ifi_flags = IFF_UP; + + const char veth_first[] = "veth_first"; + addattr(&req.hdr, sizeof(req), IFLA_IFNAME, veth_first, strlen(veth_first)); + + struct rtattr* linkinfo; + linkinfo = NLMSG_TAIL(&req.hdr); + { + addattr(&req.hdr, sizeof(req), IFLA_LINKINFO, nullptr, 0); + addattr(&req.hdr, sizeof(req), IFLA_INFO_KIND, "veth", 4); + + struct rtattr *veth_data, *peer_data; + veth_data = NLMSG_TAIL(&req.hdr); + { + addattr(&req.hdr, sizeof(req), IFLA_INFO_DATA, NULL, 0); + peer_data = NLMSG_TAIL(&req.hdr); + { + struct ifinfomsg ifm = {}; + addattr(&req.hdr, sizeof(req), VETH_INFO_PEER, &ifm, sizeof(ifm)); + const char veth_second[] = "veth_second"; + addattr(&req.hdr, sizeof(req), IFLA_IFNAME, veth_second, + strlen(veth_second)); + } + peer_data->rta_len = (uint64_t)NLMSG_TAIL(&req.hdr) - (uint64_t)peer_data; + } + veth_data->rta_len = (uint64_t)NLMSG_TAIL(&req.hdr) - (uint64_t)veth_data; + } + linkinfo->rta_len = (uint64_t)NLMSG_TAIL(&req.hdr) - (uint64_t)linkinfo; + EXPECT_NO_ERRNO(NetlinkRequestAckOrError(fd, kSeq, &req, req.hdr.nlmsg_len)); +} + } // namespace } // namespace testing