Skip to content

Commit

Permalink
loader: clean up tcx bpf_links created by newer Cilium versions
Browse files Browse the repository at this point in the history
A follow-up commit will introduce attaching TC programs using tcx. Those
attachments cannot be overridden using netlink. If an older version of
Cilium wants to replace an TC program on a managed interface, it'll need to
remove the tcx attachment first.

This commit teaches the agent to remove leftover tcx link objects from previous
installs, before reattaching it using netlink. Note that this transition is
never seamless, since some time passes between deleting the link and attaching
the new program using netlink. However, as explained in 7a8e3c8
("loader: clean up XDP bpf_links created by newer Cilium versions"), this
downgrade path should rarely happen.

Signed-off-by: Robin Gögge <r.goegge@isovalent.com>
Co-authored-by: Timo Beckers <timo@isovalent.com>
  • Loading branch information
rgo3 and ti-mo committed Mar 27, 2024
1 parent 9f06a59 commit 082e607
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 26 deletions.
10 changes: 10 additions & 0 deletions pkg/bpf/bpffs_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package bpf

import (
"errors"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -64,6 +65,15 @@ func MkdirBPF(path string) error {
return os.MkdirAll(path, 0755)
}

// Remove path ignoring ErrNotExist.
func Remove(path string) error {
err := os.RemoveAll(path)
if err != nil && !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("removing bpffs directory at %s: %w", path, err)
}
return err
}

func tcPathFromMountInfo(name string) string {
readMountInfo.Do(func() {
mountInfos, err := mountinfo.GetMountInfo()
Expand Down
6 changes: 6 additions & 0 deletions pkg/datapath/loader/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,17 @@ func (l *loader) reinitializeIPSec(ctx context.Context) error {
log.WithError(err).WithField(logfields.Interface, iface).Warn("Rpfilter could not be disabled, node to node encryption may fail")
}

device, err := netlink.LinkByName(iface)
if err != nil {
return fmt.Errorf("retrieving device %s: %w", iface, err)
}

finalize, err := replaceDatapath(ctx,
replaceDatapathOptions{
device: iface,
elf: networkObj,
programs: progs,
linkDir: bpffsDeviceLinksDir(bpf.CiliumPath(), device),
},
)
if err != nil {
Expand Down
30 changes: 28 additions & 2 deletions pkg/datapath/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,11 @@ func (l *loader) reloadHostDatapath(ctx context.Context, ep datapath.Endpoint, o
// missing all tail calls.

// Replace programs on cilium_host.
host, err := netlink.LinkByName(ep.InterfaceName())
if err != nil {
return fmt.Errorf("retrieving device %s: %w", ep.InterfaceName(), err)
}

progs := []progDefinition{
{progName: symbolToHostEp, direction: dirIngress},
{progName: symbolFromHostEp, direction: dirEgress},
Expand All @@ -393,6 +398,7 @@ func (l *loader) reloadHostDatapath(ctx context.Context, ep datapath.Endpoint, o
device: ep.InterfaceName(),
elf: objPath,
programs: progs,
linkDir: bpffsDeviceLinksDir(bpf.CiliumPath(), host),
},
)
if err != nil {
Expand All @@ -412,7 +418,8 @@ func (l *loader) reloadHostDatapath(ctx context.Context, ep datapath.Endpoint, o
defer finalize()

// Replace program on cilium_net.
if _, err := netlink.LinkByName(defaults.SecondHostDevice); err != nil {
net, err := netlink.LinkByName(defaults.SecondHostDevice)
if err != nil {
log.WithError(err).WithField("device", defaults.SecondHostDevice).Error("Link does not exist")
return fmt.Errorf("device '%s' not found: %w", defaults.SecondHostDevice, err)
}
Expand All @@ -431,6 +438,7 @@ func (l *loader) reloadHostDatapath(ctx context.Context, ep datapath.Endpoint, o
device: defaults.SecondHostDevice,
elf: secondDevObjPath,
programs: progs,
linkDir: bpffsDeviceLinksDir(bpf.CiliumPath(), net),
},
)
if err != nil {
Expand All @@ -447,7 +455,8 @@ func (l *loader) reloadHostDatapath(ctx context.Context, ep datapath.Endpoint, o

// Replace programs on physical devices.
for _, device := range option.Config.GetDevices() {
if _, err := netlink.LinkByName(device); err != nil {
iface, err := netlink.LinkByName(device)
if err != nil {
log.WithError(err).WithField("device", device).Warn("Link does not exist")
continue
}
Expand Down Expand Up @@ -482,6 +491,7 @@ func (l *loader) reloadHostDatapath(ctx context.Context, ep datapath.Endpoint, o
device: device,
elf: netdevObjPath,
programs: progs,
linkDir: bpffsDeviceLinksDir(bpf.CiliumPath(), iface),
},
)
if err != nil {
Expand Down Expand Up @@ -537,6 +547,7 @@ func (l *loader) reloadDatapath(ctx context.Context, ep datapath.Endpoint, dirs
device: ep.InterfaceName(),
elf: objPath,
programs: progs,
linkDir: bpffsEndpointLinksDir(bpf.CiliumPath(), ep),
},
)
if err != nil {
Expand Down Expand Up @@ -579,6 +590,11 @@ func (l *loader) replaceOverlayDatapath(ctx context.Context, cArgs []string, ifa
log.WithError(err).Fatal("failed to compile overlay programs")
}

device, err := netlink.LinkByName(iface)
if err != nil {
return fmt.Errorf("retrieving device %s: %w", iface, err)
}

progs := []progDefinition{
{progName: symbolFromOverlay, direction: dirIngress},
{progName: symbolToOverlay, direction: dirEgress},
Expand All @@ -589,6 +605,7 @@ func (l *loader) replaceOverlayDatapath(ctx context.Context, cArgs []string, ifa
device: iface,
elf: overlayObj,
programs: progs,
linkDir: bpffsDeviceLinksDir(bpf.CiliumPath(), device),
},
)
if err != nil {
Expand Down Expand Up @@ -722,6 +739,15 @@ func (l *loader) Unload(ep datapath.Endpoint) {
removeEndpointRoute(ep, *iputil.AddrToIPNet(ip))
}
}

// If Cilium and the kernel support tcx to attach TC programs to the
// endpoint's veth device, its bpf_link object is pinned to a per-endpoint
// bpffs directory. When the endpoint gets deleted, removing the whole
// directory cleans up any pinned maps and links.
bpffsPath := bpffsEndpointDir(bpf.CiliumPath(), ep)
if err := bpf.Remove(bpffsPath); err != nil {
log.WithError(err).WithField(logfields.EndpointID, ep.StringID())
}
}

// EndpointHash hashes the specified endpoint configuration with the current
Expand Down
24 changes: 10 additions & 14 deletions pkg/datapath/loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,23 +184,17 @@ func (s *LoaderTestSuite) TestReload(c *C) {
{progName: symbolFromEndpoint, direction: dirIngress},
{progName: symbolToEndpoint, direction: dirEgress},
}
finalize, err := replaceDatapath(ctx,
replaceDatapathOptions{
device: ep.InterfaceName(),
elf: objPath,
programs: progs,
},
)
opts := replaceDatapathOptions{
device: ep.InterfaceName(),
elf: objPath,
programs: progs,
linkDir: testutils.TempBPFFS(c),
}
finalize, err := replaceDatapath(ctx, opts)
c.Assert(err, IsNil)
finalize()

finalize, err = replaceDatapath(ctx,
replaceDatapathOptions{
device: ep.InterfaceName(),
elf: objPath,
programs: progs,
},
)
finalize, err = replaceDatapath(ctx, opts)

c.Assert(err, IsNil)
finalize()
Expand Down Expand Up @@ -343,6 +337,7 @@ func BenchmarkReplaceDatapath(b *testing.B) {
}

objPath := fmt.Sprintf("%s/%s", dirInfo.Output, endpointObj)
linkDir := testutils.TempBPFFS(b)
progs := []progDefinition{{progName: symbolFromEndpoint, direction: dirIngress}}
b.ResetTimer()
for i := 0; i < b.N; i++ {
Expand All @@ -351,6 +346,7 @@ func BenchmarkReplaceDatapath(b *testing.B) {
device: ep.InterfaceName(),
elf: objPath,
programs: progs,
linkDir: linkDir,
},
)
if err != nil {
Expand Down
35 changes: 27 additions & 8 deletions pkg/datapath/loader/netlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"net"
"os"
"path/filepath"
"strings"

"github.com/vishvananda/netlink"
Expand Down Expand Up @@ -66,6 +67,7 @@ type replaceDatapathOptions struct {
elf string // path to object file
programs []progDefinition // programs that we want to attach/replace
xdpMode string // XDP driver mode, only applies when attaching XDP programs
linkDir string // path to bpffs dir holding bpf_links for the device/endpoint
}

// replaceDatapath replaces the qdisc and BPF program for an endpoint or XDP program.
Expand All @@ -87,6 +89,10 @@ func replaceDatapath(ctx context.Context, opts replaceDatapathOptions) (_ func()
return nil, err
}

if opts.linkDir == "" {
return nil, errors.New("opts.linkDir not set in replaceDatapath")
}

link, err := netlink.LinkByName(opts.device)
if err != nil {
return nil, fmt.Errorf("getting interface %s by name: %w", opts.device, err)
Expand Down Expand Up @@ -230,17 +236,17 @@ func replaceDatapath(ctx context.Context, opts replaceDatapathOptions) (_ func()
// flow in.
for _, prog := range opts.programs {
scopedLog := l.WithField("progName", prog.progName).WithField("direction", prog.direction)
if opts.xdpMode != "" {
linkDir := bpffsDeviceLinksDir(bpf.CiliumPath(), link)
if err := bpf.MkdirBPF(linkDir); err != nil {
return nil, fmt.Errorf("creating bpffs link dir for device %s: %w", link.Attrs().Name, err)
}

if err := bpf.MkdirBPF(opts.linkDir); err != nil {
return nil, fmt.Errorf("creating bpffs link dir for device %s: %w", link.Attrs().Name, err)
}

if opts.xdpMode != "" {
scopedLog.Debug("Attaching XDP program to interface")
err = attachXDPProgram(link, coll.Programs[prog.progName], prog.progName, linkDir, xdpConfigModeToFlag(opts.xdpMode))
err = attachXDPProgram(link, coll.Programs[prog.progName], prog.progName, opts.linkDir, xdpConfigModeToFlag(opts.xdpMode))
} else {
scopedLog.Debug("Attaching TC program to interface")
err = attachTCProgram(link, coll.Programs[prog.progName], prog.progName, directionToParent(prog.direction))
err = attachTCProgram(link, coll.Programs[prog.progName], prog.progName, opts.linkDir, directionToParent(prog.direction))
}

if err != nil {
Expand Down Expand Up @@ -283,11 +289,24 @@ func resolveAndInsertCalls(coll *ebpf.Collection, mapName string, calls []ebpf.M
}

// attachTCProgram attaches the TC program 'prog' to link.
func attachTCProgram(link netlink.Link, prog *ebpf.Program, progName string, qdiscParent uint32) error {
func attachTCProgram(link netlink.Link, prog *ebpf.Program, progName, bpffsDir string, qdiscParent uint32) error {
if prog == nil {
return errors.New("cannot attach a nil program")
}

// Remove tcx bpf_links created by newer versions of Cilium. They cannot be
// overwritten by netlink-based tc attachments, as tcx is a separate hook
// altogether. Remove the tcx link first to avoid tc programs being run twice
// for every packet. This cannot be done seamlessly and will cause a small
// window of connection interruption.
pin := filepath.Join(bpffsDir, progName)
if err := os.Remove(pin); err == nil {
log.WithField("device", link.Attrs().Name).WithField("pinPath", pin).
Info("Removed tcx link before legacy tc downgrade, possible connectivity interruption")
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("unpinning defunct link %s: %w", pin, err)
}

if err := replaceQdisc(link); err != nil {
return fmt.Errorf("replacing clsact qdisc for interface %s: %w", link.Attrs().Name, err)
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/datapath/loader/netlink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,8 @@ func TestAttachRemoveTCProgram(t *testing.T) {

prog := mustTCProgram(t)

err = attachTCProgram(dummy, prog, "test", directionToParent(dirEgress))
bpffs := testutils.TempBPFFS(t)
err = attachTCProgram(dummy, prog, "test", bpffsDeviceLinksDir(bpffs, dummy), directionToParent(dirEgress))
require.NoError(t, err)

filters, err := netlink.FilterList(dummy, directionToParent(dirEgress))
Expand Down
42 changes: 41 additions & 1 deletion pkg/datapath/loader/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"path/filepath"

"github.com/vishvananda/netlink"

datapath "github.com/cilium/cilium/pkg/datapath/types"
)

// bpffsDevicesDir returns the path to the 'devices' directory on bpffs, usually
Expand All @@ -18,12 +20,50 @@ func bpffsDevicesDir(base string) string {
return filepath.Join(base, "devices")
}

// bpffsDeviceDir returns the path to the per-device directory on bpffs, usually
// /sys/fs/bpf/cilium/devices/<device>. It does not ensure the directory exists.
//
// base is typically set to /sys/fs/bpf/cilium, but can be a temp directory
// during tests.
func bpffsDeviceDir(base string, device netlink.Link) string {
return filepath.Join(bpffsDevicesDir(base), device.Attrs().Name)
}

// bpffsDeviceLinksDir returns the bpffs path to the per-device links directory,
// usually /sys/fs/bpf/cilium/devices/<device>/links. It does not ensure the
// directory exists.
//
// base is typically set to /sys/fs/bpf/cilium, but can be a temp directory
// during tests.
func bpffsDeviceLinksDir(base string, device netlink.Link) string {
return filepath.Join(bpffsDevicesDir(base), device.Attrs().Name, "links")
return filepath.Join(bpffsDeviceDir(base, device), "links")
}

// bpffsEndpointsDir returns the path to the 'endpoints' directory on bpffs, usually
// /sys/fs/bpf/cilium/endpoints. It does not ensure the directory exists.
//
// base is typically set to /sys/fs/bpf/cilium, but can be a temp directory
// during tests.
func bpffsEndpointsDir(base string) string {
return filepath.Join(base, "endpoints")
}

// bpffsEndpointDir returns the path to the per-endpoint directory on bpffs,
// usually /sys/fs/bpf/cilium/endpoints/<endpoint-id>. It does not ensure the
// directory exists.
//
// base is typically set to /sys/fs/bpf/cilium, but can be a temp directory
// during tests.
func bpffsEndpointDir(base string, ep datapath.Endpoint) string {
return filepath.Join(bpffsEndpointsDir(base), ep.StringID())
}

// bpffsEndpointLinksDir returns the bpffs path to the per-endpoint links directory,
// usually /sys/fs/bpf/cilium/endpoints/<endpoint-id>/links. It does not ensure the
// directory exists.
//
// base is typically set to /sys/fs/bpf/cilium, but can be a temp directory
// during tests.
func bpffsEndpointLinksDir(base string, ep datapath.Endpoint) string {
return filepath.Join(bpffsEndpointDir(base, ep), "links")
}
6 changes: 6 additions & 0 deletions pkg/datapath/loader/xdp.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,19 @@ func compileAndLoadXDPProg(ctx context.Context, xdpDev, xdpMode string, extraCAr
return err
}

iface, err := netlink.LinkByName(xdpDev)
if err != nil {
return fmt.Errorf("retrieving device %s: %w", xdpDev, err)
}

progs := []progDefinition{{progName: symbolFromHostNetdevXDP, direction: ""}}
finalize, err := replaceDatapath(ctx,
replaceDatapathOptions{
device: xdpDev,
elf: objPath,
programs: progs,
xdpMode: xdpMode,
linkDir: bpffsDeviceLinksDir(bpf.CiliumPath(), iface),
},
)
if err != nil {
Expand Down

0 comments on commit 082e607

Please sign in to comment.