Repository: cilium/ebpf
Pull request #108 (2020-06-12 09:31:09)

Add support for raw bpf_link
bpf_link appeared in 5.7 and there is a push to migrate existing BPF hooks to using this infrastructure. For example, bpf_iter is only useable via bpf_link. This PR adds a new subpackage link. It contains low-level APIs to do attaching. I'll submit the higher-level APIs based on `Link` in a follow up to this PR.

```
package link // import "github.com/cilium/ebpf/link"

var ErrNotSupported = internal.ErrNotSupported
func RawAttachProgram(opts RawAttachProgramOptions) error
func RawDetachProgram(opts RawDetachProgramOptions) error
type Link interface{ ... }
type RawAttachProgramOptions struct{ ... }
type RawDetachProgramOptions struct{ ... }
type RawLink struct{ ... }
    func AttachRawLink(opts RawLinkOptions) (*RawLink, error)
    func LoadPinnedRawLink(fileName string) (*RawLink, error)
type RawLinkOptions struct{ ... }
type RawLinkUpdateOptions struct{ ... }
```

tklauser: Not sure about the CI tests failing with Go 1.14/Kernel 4.19 (https://ebpf.semaphoreci.com/workflows/f9fafea7-370c-4a02-b588-262dd87076d5) but I assume it's a flake?
lmb: @jrfastab the follow up to this includes actual usage of the API: https://github.com/cilium/ebpf/tree/iter-and-cgroup (top two commits)

Patch:
diff --git a/internal/syscall.go b/internal/syscall.go
index 586f2e4..b9de045 100644
--- a/internal/syscall.go
+++ b/internal/syscall.go
@@ -1,10 +1,12 @@
 package internal
 
 import (
+	"path/filepath"
 	"runtime"
 	"unsafe"
 
 	"github.com/cilium/ebpf/internal/unix"
+	"golang.org/x/xerrors"
 )
 
 //go:generate stringer -output syscall_string.go -type=BPFCmd
@@ -64,3 +66,74 @@ func BPF(cmd BPFCmd, attr unsafe.Pointer, size uintptr) (uintptr, error) {
 
 	return r1, err
 }
+
+type BPFProgAttachAttr struct {
+	TargetFd     uint32
+	AttachBpfFd  uint32
+	AttachType   uint32
+	AttachFlags  uint32
+	ReplaceBpfFd uint32
+}
+
+func BPFProgAttach(attr *BPFProgAttachAttr) error {
+	_, err := BPF(BPF_PROG_ATTACH, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	return err
+}
+
+type BPFProgDetachAttr struct {
+	TargetFd    uint32
+	AttachBpfFd uint32
+	AttachType  uint32
+}
+
+func BPFProgDetach(attr *BPFProgDetachAttr) error {
+	_, err := BPF(BPF_PROG_DETACH, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	return err
+}
+
+type bpfObjAttr struct {
+	fileName  Pointer
+	fd        uint32
+	fileFlags uint32
+}
+
+const bpfFSType = 0xcafe4a11
+
+// BPFObjPin wraps BPF_OBJ_PIN.
+func BPFObjPin(fileName string, fd *FD) error {
+	dirName := filepath.Dir(fileName)
+	var statfs unix.Statfs_t
+	if err := unix.Statfs(dirName, &statfs); err != nil {
+		return err
+	}
+	if uint64(statfs.Type) != bpfFSType {
+		return xerrors.Errorf("%s is not on a bpf filesystem", fileName)
+	}
+
+	value, err := fd.Value()
+	if err != nil {
+		return err
+	}
+
+	attr := bpfObjAttr{
+		fileName: NewStringPointer(fileName),
+		fd:       value,
+	}
+	_, err = BPF(BPF_OBJ_PIN, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	if err != nil {
+		return xerrors.Errorf("pin object %s: %w", fileName, err)
+	}
+	return nil
+}
+
+// BPFObjGet wraps BPF_OBJ_GET.
+func BPFObjGet(fileName string) (*FD, error) {
+	attr := bpfObjAttr{
+		fileName: NewStringPointer(fileName),
+	}
+	ptr, err := BPF(BPF_OBJ_GET, unsafe.Pointer(&attr), unsafe.Sizeof(attr))
+	if err != nil {
+		return nil, xerrors.Errorf("get object %s: %w", fileName, err)
+	}
+	return NewFD(uint32(ptr)), nil
+}
diff --git a/internal/unix/types_linux.go b/internal/unix/types_linux.go
index 0545fe9..9363d0b 100644
--- a/internal/unix/types_linux.go
+++ b/internal/unix/types_linux.go
@@ -16,6 +16,7 @@ const (
 	EINVAL                   = linux.EINVAL
 	EPOLLIN                  = linux.EPOLLIN
 	EINTR                    = linux.EINTR
+	EPERM                    = linux.EPERM
 	ESRCH                    = linux.ESRCH
 	ENODEV                   = linux.ENODEV
 	BPF_F_RDONLY_PROG        = linux.BPF_F_RDONLY_PROG
diff --git a/internal/unix/types_other.go b/internal/unix/types_other.go
index 555a5ce..2dea950 100644
--- a/internal/unix/types_other.go
+++ b/internal/unix/types_other.go
@@ -17,6 +17,7 @@ const (
 	ENOSPC                   = syscall.ENOSPC
 	EINVAL                   = syscall.EINVAL
 	EINTR                    = syscall.EINTR
+	EPERM                    = syscall.EPERM
 	ESRCH                    = syscall.ESRCH
 	ENODEV                   = syscall.ENODEV
 	BPF_F_RDONLY_PROG        = 0
diff --git a/link/link.go b/link/link.go
new file mode 100644
index 0000000..911ec83
--- /dev/null
+++ b/link/link.go
@@ -0,0 +1,152 @@
+package link
+
+import (
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal"
+
+	"golang.org/x/xerrors"
+)
+
+var ErrNotSupported = internal.ErrNotSupported
+
+// Link represents a Program attached to a BPF hook.
+type Link interface {
+	// Replace the current program with a new program.
+	//
+	// Passing a nil program is an error.
+	Update(*ebpf.Program) error
+
+	// Persist a link by pinning it into a bpffs.
+	//
+	// May return an error wrapping ErrNotSupported.
+	Pin(string) error
+
+	// Close frees resources.
+	//
+	// The link will be broken unless it has been pinned. A link
+	// may continue past the lifetime of the process if Close is
+	// not called.
+	Close() error
+
+	// Prevent external users from implementing this interface.
+	isLink()
+}
+
+// RawLinkOptions control the creation of a raw link.
+type RawLinkOptions struct {
+	// File descriptor to attach to. This differs for each attach type.
+	Target int
+	// Program to attach.
+	Program *ebpf.Program
+	// Attach must match the attach type of Program.
+	Attach ebpf.AttachType
+}
+
+// RawLink is the low-level API to bpf_link.
+//
+// You should consider using the higher level interfaces in this
+// package instead.
+type RawLink struct {
+	fd *internal.FD
+}
+
+// AttachRawLink creates a raw link.
+func AttachRawLink(opts RawLinkOptions) (*RawLink, error) {
+	if err := haveBPFLink(); err != nil {
+		return nil, err
+	}
+
+	if opts.Target < 0 {
+		return nil, xerrors.Errorf("invalid target: %s", internal.ErrClosedFd)
+	}
+
+	progFd := opts.Program.FD()
+	if progFd < 0 {
+		return nil, xerrors.Errorf("invalid program: %s", internal.ErrClosedFd)
+	}
+
+	attr := bpfLinkCreateAttr{
+		targetFd:   uint32(opts.Target),
+		progFd:     uint32(progFd),
+		attachType: opts.Attach,
+	}
+	fd, err := bpfLinkCreate(&attr)
+	if err != nil {
+		return nil, xerrors.Errorf("can't create link: %s", err)
+	}
+
+	return &RawLink{fd}, nil
+}
+
+// LoadPinnedRawLink loads a persisted link from a bpffs.
+func LoadPinnedRawLink(fileName string) (*RawLink, error) {
+	fd, err := internal.BPFObjGet(fileName)
+	if err != nil {
+		return nil, xerrors.Errorf("can't load pinned link: %s", err)
+	}
+
+	return &RawLink{fd}, nil
+}
+
+func (l *RawLink) isLink() {}
+
+// Close breaks the link.
+//
+// Use Pin if you want to make the link persistent.
+func (l *RawLink) Close() error {
+	return l.fd.Close()
+}
+
+// Pin persists a link past the lifetime of the process.
+//
+// Calling Close on a pinned Link will not break the link
+// until the pin is removed.
+func (l *RawLink) Pin(fileName string) error {
+	if err := internal.BPFObjPin(fileName, l.fd); err != nil {
+		return xerrors.Errorf("can't pin link: %s", err)
+	}
+	return nil
+}
+
+// Update implements Link.
+func (l *RawLink) Update(new *ebpf.Program) error {
+	return l.UpdateArgs(RawLinkUpdateOptions{
+		New: new,
+	})
+}
+
+// RawLinkUpdateOptions control the behaviour of RawLink.UpdateArgs.
+type RawLinkUpdateOptions struct {
+	New   *ebpf.Program
+	Old   *ebpf.Program
+	Flags uint32
+}
+
+// UpdateArgs updates a link based on args.
+func (l *RawLink) UpdateArgs(opts RawLinkUpdateOptions) error {
+	newFd := opts.New.FD()
+	if newFd < 0 {
+		return xerrors.Errorf("invalid program: %s", internal.ErrClosedFd)
+	}
+
+	var oldFd int
+	if opts.Old != nil {
+		oldFd = opts.Old.FD()
+		if oldFd < 0 {
+			return xerrors.Errorf("invalid replacement program: %s", internal.ErrClosedFd)
+		}
+	}
+
+	linkFd, err := l.fd.Value()
+	if err != nil {
+		return xerrors.Errorf("can't update link: %s", err)
+	}
+
+	attr := bpfLinkUpdateAttr{
+		linkFd:    linkFd,
+		newProgFd: uint32(newFd),
+		oldProgFd: uint32(oldFd),
+		flags:     opts.Flags,
+	}
+	return bpfLinkUpdate(&attr)
+}
diff --git a/link/program.go b/link/program.go
new file mode 100644
index 0000000..7c7d347
--- /dev/null
+++ b/link/program.go
@@ -0,0 +1,76 @@
+package link
+
+import (
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/internal"
+
+	"golang.org/x/xerrors"
+)
+
+type RawAttachProgramOptions struct {
+	// File descriptor to attach to. This differs for each attach type.
+	Target int
+	// Program to attach.
+	Program *ebpf.Program
+	// Program to replace (cgroups).
+	Replace *ebpf.Program
+	// Attach must match the attach type of Program (and Replace).
+	Attach ebpf.AttachType
+	// Flags control the attach behaviour. This differs for each attach type.
+	Flags uint32
+}
+
+// RawAttachProgram is a low level wrapper around BPF_PROG_ATTACH.
+//
+// You should use one of the higher level abstractions available in this
+// package if possible.
+func RawAttachProgram(opts RawAttachProgramOptions) error {
+	if err := haveProgAttach(); err != nil {
+		return err
+	}
+
+	var replaceFd uint32
+	if opts.Replace != nil {
+		replaceFd = uint32(opts.Replace.FD())
+	}
+
+	attr := internal.BPFProgAttachAttr{
+		TargetFd:     uint32(opts.Target),
+		AttachBpfFd:  uint32(opts.Program.FD()),
+		ReplaceBpfFd: replaceFd,
+		AttachType:   uint32(opts.Attach),
+		AttachFlags:  uint32(opts.Flags),
+	}
+
+	if err := internal.BPFProgAttach(&attr); err != nil {
+		return xerrors.Errorf("can't attach program: %s", err)
+	}
+	return nil
+}
+
+type RawDetachProgramOptions struct {
+	Target  int
+	Program *ebpf.Program
+	Attach  ebpf.AttachType
+}
+
+// RawDetachProgram is a low level wrapper around BPF_PROG_DETACH.
+//
+// You should use one of the higher level abstractions available in this
+// package if possible.
+func RawDetachProgram(opts RawDetachProgramOptions) error {
+	if err := haveProgAttach(); err != nil {
+		return err
+	}
+
+	attr := internal.BPFProgDetachAttr{
+		TargetFd:    uint32(opts.Target),
+		AttachBpfFd: uint32(opts.Program.FD()),
+		AttachType:  uint32(opts.Attach),
+	}
+	if err := internal.BPFProgDetach(&attr); err != nil {
+		return xerrors.Errorf("can't detach program: %s", err)
+	}
+
+	return nil
+}
diff --git a/link/syscalls.go b/link/syscalls.go
new file mode 100644
index 0000000..0c40750
--- /dev/null
+++ b/link/syscalls.go
@@ -0,0 +1,85 @@
+package link
+
+import (
+	"unsafe"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/asm"
+	"github.com/cilium/ebpf/internal"
+	"github.com/cilium/ebpf/internal/unix"
+
+	"golang.org/x/xerrors"
+)
+
+var haveProgAttach = internal.FeatureTest("BPF_PROG_ATTACH", "4.10", func() (bool, error) {
+	prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
+		Type:       ebpf.CGroupSKB,
+		AttachType: ebpf.AttachCGroupInetIngress,
+		License:    "MIT",
+		Instructions: asm.Instructions{
+			asm.Mov.Imm(asm.R0, 0),
+			asm.Return(),
+		},
+	})
+	if err != nil {
+		return false, nil
+	}
+
+	// BPF_PROG_ATTACH was introduced at the same time as CGgroupSKB,
+	// so being able to load the program is enough to infer that we
+	// have the syscall.
+	prog.Close()
+	return true, nil
+})
+
+type bpfLinkCreateAttr struct {
+	progFd     uint32
+	targetFd   uint32
+	attachType ebpf.AttachType
+	flags      uint32
+}
+
+func bpfLinkCreate(attr *bpfLinkCreateAttr) (*internal.FD, error) {
+	ptr, err := internal.BPF(internal.BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	if err != nil {
+		return nil, err
+	}
+	return internal.NewFD(uint32(ptr)), nil
+}
+
+type bpfLinkUpdateAttr struct {
+	linkFd    uint32
+	newProgFd uint32
+	flags     uint32
+	oldProgFd uint32
+}
+
+func bpfLinkUpdate(attr *bpfLinkUpdateAttr) error {
+	_, err := internal.BPF(internal.BPF_LINK_UPDATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
+	return err
+}
+
+var haveBPFLink = internal.FeatureTest("bpf_link", "5.7", func() (bool, error) {
+	prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
+		Type:       ebpf.CGroupSKB,
+		AttachType: ebpf.AttachCGroupInetIngress,
+		License:    "MIT",
+		Instructions: asm.Instructions{
+			asm.Mov.Imm(asm.R0, 0),
+			asm.Return(),
+		},
+	})
+	if err != nil {
+		return false, nil
+	}
+	defer prog.Close()
+
+	attr := bpfLinkCreateAttr{
+		// This is a hopefully invalid file descriptor, which triggers EBADF.
+		targetFd:   ^uint32(0),
+		progFd:     uint32(prog.FD()),
+		attachType: ebpf.AttachCGroupInetIngress,
+	}
+	_, err = bpfLinkCreate(&attr)
+	return !xerrors.Is(err, unix.EINVAL), nil
+})
diff --git a/map.go b/map.go
index 96c630f..31abc17 100644
--- a/map.go
+++ b/map.go
@@ -547,7 +547,7 @@ func (m *Map) Clone() (*Map, error) {
 //
 // This requires bpffs to be mounted above fileName. See http://cilium.readthedocs.io/en/doc-1.0/kubernetes/install/#mounting-the-bpf-fs-optional
 func (m *Map) Pin(fileName string) error {
-	return bpfPinObject(fileName, m.fd)
+	return internal.BPFObjPin(fileName, m.fd)
 }
 
 // Freeze prevents a map to be modified from user space.
@@ -578,7 +578,7 @@ func (m *Map) populate(contents []MapKV) error {
 // The function is not compatible with nested maps.
 // Use LoadPinnedMapExplicit in these situations.
 func LoadPinnedMap(fileName string) (*Map, error) {
-	fd, err := bpfGetObject(fileName)
+	fd, err := internal.BPFObjGet(fileName)
 	if err != nil {
 		return nil, err
 	}
@@ -592,7 +592,7 @@ func LoadPinnedMap(fileName string) (*Map, error) {
 
 // LoadPinnedMapExplicit loads a map with explicit parameters.
 func LoadPinnedMapExplicit(fileName string, abi *MapABI) (*Map, error) {
-	fd, err := bpfGetObject(fileName)
+	fd, err := internal.BPFObjGet(fileName)
 	if err != nil {
 		return nil, err
 	}
diff --git a/prog.go b/prog.go
index 68e239d..279992a 100644
--- a/prog.go
+++ b/prog.go
@@ -287,7 +287,7 @@ func (p *Program) Clone() (*Program, error) {
 //
 // This requires bpffs to be mounted above fileName. See http://cilium.readthedocs.io/en/doc-1.0/kubernetes/install/#mounting-the-bpf-fs-optional
 func (p *Program) Pin(fileName string) error {
-	if err := bpfPinObject(fileName, p.fd); err != nil {
+	if err := internal.BPFObjPin(fileName, p.fd); err != nil {
 		return xerrors.Errorf("can't pin program: %w", err)
 	}
 	return nil
@@ -454,7 +454,9 @@ func (p *Program) MarshalBinary() ([]byte, error) {
 	return buf, nil
 }
 
-// Attach a Program to a container object fd
+// Attach a Program.
+//
+// Deprecated: use link.RawAttachProgram instead.
 func (p *Program) Attach(fd int, typ AttachType, flags AttachFlags) error {
 	if fd < 0 {
 		return xerrors.New("invalid fd")
@@ -465,42 +467,47 @@ func (p *Program) Attach(fd int, typ AttachType, flags AttachFlags) error {
 		return err
 	}
 
-	attr := bpfProgAlterAttr{
-		targetFd:    uint32(fd),
-		attachBpfFd: pfd,
-		attachType:  uint32(typ),
-		attachFlags: uint32(flags),
+	attr := internal.BPFProgAttachAttr{
+		TargetFd:    uint32(fd),
+		AttachBpfFd: pfd,
+		AttachType:  uint32(typ),
+		AttachFlags: uint32(flags),
 	}
 
-	return bpfProgAlter(internal.BPF_PROG_ATTACH, &attr)
+	return internal.BPFProgAttach(&attr)
 }
 
-// Detach a Program from a container object fd
+// Detach a Program.
+//
+// Deprecated: use link.RawDetachProgram instead.
 func (p *Program) Detach(fd int, typ AttachType, flags AttachFlags) error {
 	if fd < 0 {
 		return xerrors.New("invalid fd")
 	}
 
+	if flags != 0 {
+		return xerrors.New("flags must be zero")
+	}
+
 	pfd, err := p.fd.Value()
 	if err != nil {
 		return err
 	}
 
-	attr := bpfProgAlterAttr{
-		targetFd:    uint32(fd),
-		attachBpfFd: pfd,
-		attachType:  uint32(typ),
-		attachFlags: uint32(flags),
+	attr := internal.BPFProgDetachAttr{
+		TargetFd:    uint32(fd),
+		AttachBpfFd: pfd,
+		AttachType:  uint32(typ),
 	}
 
-	return bpfProgAlter(internal.BPF_PROG_DETACH, &attr)
+	return internal.BPFProgDetach(&attr)
 }
 
 // LoadPinnedProgram loads a Program from a BPF file.
 //
 // Requires at least Linux 4.11.
 func LoadPinnedProgram(fileName string) (*Program, error) {
-	fd, err := bpfGetObject(fileName)
+	fd, err := internal.BPFObjGet(fileName)
 	if err != nil {
 		return nil, err
 	}
diff --git a/syscalls.go b/syscalls.go
index 628f689..399501c 100644
--- a/syscalls.go
+++ b/syscalls.go
@@ -2,7 +2,6 @@ package ebpf
 
 import (
 	"os"
-	"path/filepath"
 	"unsafe"
 
 	"github.com/cilium/ebpf/internal"
@@ -80,12 +79,6 @@ type bpfMapInfo struct {
 	mapName    bpfObjName // since 4.15 ad5b177bd73f
 }
 
-type bpfPinObjAttr struct {
-	fileName internal.Pointer
-	fd       uint32
-	padding  uint32
-}
-
 type bpfProgLoadAttr struct {
 	progType           ProgramType
 	insCount           uint32
@@ -134,13 +127,6 @@ type bpfProgTestRunAttr struct {
 	duration    uint32
 }
 
-type bpfProgAlterAttr struct {
-	targetFd    uint32
-	attachBpfFd uint32
-	attachType  uint32
-	attachFlags uint32
-}
-
 type bpfObjGetInfoByFDAttr struct {
 	fd      uint32
 	infoLen uint32
@@ -184,11 +170,6 @@ func bpfProgTestRun(attr *bpfProgTestRunAttr) error {
 	return err
 }
 
-func bpfProgAlter(cmd internal.BPFCmd, attr *bpfProgAlterAttr) error {
-	_, err := internal.BPF(cmd, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
-	return err
-}
-
 func bpfMapCreate(attr *bpfMapCreateAttr) (*internal.FD, error) {
 	fd, err := internal.BPF(internal.BPF_MAP_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
 	if xerrors.Is(err, os.ErrPermission) {
@@ -370,43 +351,6 @@ func bpfMapFreeze(m *internal.FD) error {
 	return err
 }
 
-const bpfFSType = 0xcafe4a11
-
-func bpfPinObject(fileName string, fd *internal.FD) error {
-	dirName := filepath.Dir(fileName)
-	var statfs unix.Statfs_t
-	if err := unix.Statfs(dirName, &statfs); err != nil {
-		return err
-	}
-	if uint64(statfs.Type) != bpfFSType {
-		return xerrors.Errorf("%s is not on a bpf filesystem", fileName)
-	}
-
-	value, err := fd.Value()
-	if err != nil {
-		return err
-	}
-
-	_, err = internal.BPF(internal.BPF_OBJ_PIN, unsafe.Pointer(&bpfPinObjAttr{
-		fileName: internal.NewStringPointer(fileName),
-		fd:       value,
-	}), 16)
-	if err != nil {
-		return xerrors.Errorf("pin object %s: %w", fileName, err)
-	}
-	return nil
-}
-
-func bpfGetObject(fileName string) (*internal.FD, error) {
-	ptr, err := internal.BPF(internal.BPF_OBJ_GET, unsafe.Pointer(&bpfPinObjAttr{
-		fileName: internal.NewStringPointer(fileName),
-	}), 16)
-	if err != nil {
-		return nil, xerrors.Errorf("get object %s: %w", fileName, err)
-	}
-	return internal.NewFD(uint32(ptr)), nil
-}
-
 func bpfGetObjectInfoByFD(fd *internal.FD, info unsafe.Pointer, size uintptr) error {
 	value, err := fd.Value()
 	if err != nil {
diff --git a/types.go b/types.go
index 5bc5f65..1ffc621 100644
--- a/types.go
+++ b/types.go
@@ -187,6 +187,9 @@ const (
 	AttachTraceRawTp
 	AttachTraceFEntry
 	AttachTraceFExit
+	AttachModifyReturn
+	AttachLSMMac
+	AttachTraceIter
 )
 
 // AttachFlags of the eBPF program used in BPF_PROG_ATTACH command
diff --git a/types_string.go b/types_string.go
index 226cf4e..c713957 100644
--- a/types_string.go
+++ b/types_string.go
@@ -120,11 +120,14 @@ func _() {
 	_ = x[AttachTraceRawTp-23]
 	_ = x[AttachTraceFEntry-24]
 	_ = x[AttachTraceFExit-25]
+	_ = x[AttachModifyReturn-26]
+	_ = x[AttachLSMMac-27]
+	_ = x[AttachTraceIter-28]
 }
 
-const _AttachType_name = "AttachNoneAttachCGroupInetEgressAttachCGroupInetSockCreateAttachCGroupSockOpsAttachSkSKBStreamParserAttachSkSKBStreamVerdictAttachCGroupDeviceAttachSkMsgVerdictAttachCGroupInet4BindAttachCGroupInet6BindAttachCGroupInet4ConnectAttachCGroupInet6ConnectAttachCGroupInet4PostBindAttachCGroupInet6PostBindAttachCGroupUDP4SendmsgAttachCGroupUDP6SendmsgAttachLircMode2AttachFlowDissectorAttachCGroupSysctlAttachCGroupUDP4RecvmsgAttachCGroupUDP6RecvmsgAttachCGroupGetsockoptAttachCGroupSetsockoptAttachTraceRawTpAttachTraceFEntryAttachTraceFExit"
+const _AttachType_name = "AttachNoneAttachCGroupInetEgressAttachCGroupInetSockCreateAttachCGroupSockOpsAttachSkSKBStreamParserAttachSkSKBStreamVerdictAttachCGroupDeviceAttachSkMsgVerdictAttachCGroupInet4BindAttachCGroupInet6BindAttachCGroupInet4ConnectAttachCGroupInet6ConnectAttachCGroupInet4PostBindAttachCGroupInet6PostBindAttachCGroupUDP4SendmsgAttachCGroupUDP6SendmsgAttachLircMode2AttachFlowDissectorAttachCGroupSysctlAttachCGroupUDP4RecvmsgAttachCGroupUDP6RecvmsgAttachCGroupGetsockoptAttachCGroupSetsockoptAttachTraceRawTpAttachTraceFEntryAttachTraceFExitAttachModifyReturnAttachLSMMacAttachTraceIter"
 
-var _AttachType_index = [...]uint16{0, 10, 32, 58, 77, 100, 124, 142, 160, 181, 202, 226, 250, 275, 300, 323, 346, 361, 380, 398, 421, 444, 466, 488, 504, 521, 537}
+var _AttachType_index = [...]uint16{0, 10, 32, 58, 77, 100, 124, 142, 160, 181, 202, 226, 250, 275, 300, 323, 346, 361, 380, 398, 421, 444, 466, 488, 504, 521, 537, 555, 567, 582}
 
 func (i AttachType) String() string {
 	if i >= AttachType(len(_AttachType_index)-1) {


Test patch:
diff --git a/internal/testutils/feature.go b/internal/testutils/feature.go
index 49f49c2..b02b680 100644
--- a/internal/testutils/feature.go
+++ b/internal/testutils/feature.go
@@ -57,6 +57,9 @@ func SkipIfNotSupported(tb testing.TB, err error) {
 		checkKernelVersion(tb, ufe)
 		tb.Skip(ufe.Error())
 	}
+	if xerrors.Is(err, internal.ErrNotSupported) {
+		tb.Skip(err.Error())
+	}
 }
 
 func checkKernelVersion(tb testing.TB, ufe *internal.UnsupportedFeatureError) {
diff --git a/link/link_test.go b/link/link_test.go
new file mode 100644
index 0000000..b53acef
--- /dev/null
+++ b/link/link_test.go
@@ -0,0 +1,141 @@
+package link
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"testing"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/asm"
+	"github.com/cilium/ebpf/internal/testutils"
+
+	"golang.org/x/xerrors"
+)
+
+func TestRawLink(t *testing.T) {
+	cgroup, prog, cleanup := mustCgroupFixtures(t)
+	defer cleanup()
+
+	link, err := AttachRawLink(RawLinkOptions{
+		Target:  int(cgroup.Fd()),
+		Program: prog,
+		Attach:  ebpf.AttachCGroupInetEgress,
+	})
+	testutils.SkipIfNotSupported(t, err)
+	if err != nil {
+		t.Fatal("Can't create raw link:", err)
+	}
+
+	testLink(t, link, testLinkOptions{
+		prog: prog,
+		loadPinned: func(f string) (Link, error) {
+			return LoadPinnedRawLink(f)
+		},
+	})
+}
+
+func mustCgroupFixtures(t *testing.T) (*os.File, *ebpf.Program, func()) {
+	t.Helper()
+
+	testutils.SkipIfNotSupported(t, haveProgAttach())
+
+	prog := mustCgroupEgressProgram(t)
+	cgdir, err := ioutil.TempDir("/sys/fs/cgroup/unified", "ebpf-link")
+	if err != nil {
+		prog.Close()
+		t.Fatal("Can't create cgroupv2:", err)
+	}
+
+	cgroup, err := os.Open(cgdir)
+	if err != nil {
+		prog.Close()
+		os.Remove(cgdir)
+		t.Fatal(err)
+	}
+
+	return cgroup, prog, func() {
+		prog.Close()
+		cgroup.Close()
+		os.Remove(cgdir)
+	}
+}
+
+func mustCgroupEgressProgram(t *testing.T) *ebpf.Program {
+	t.Helper()
+
+	prog, err := ebpf.NewProgram(&ebpf.ProgramSpec{
+		Type:       ebpf.CGroupSKB,
+		AttachType: ebpf.AttachCGroupInetEgress,
+		License:    "MIT",
+		Instructions: asm.Instructions{
+			asm.Mov.Imm(asm.R0, 0),
+			asm.Return(),
+		},
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	return prog
+}
+
+type testLinkOptions struct {
+	prog       *ebpf.Program
+	loadPinned func(string) (Link, error)
+}
+
+func testLink(t *testing.T, link Link, opts testLinkOptions) {
+	t.Helper()
+
+	tmp, err := ioutil.TempDir("/sys/fs/bpf", "ebpf-test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmp)
+
+	path := filepath.Join(tmp, "link")
+	err = link.Pin(path)
+	if err == ErrNotSupported {
+		t.Errorf("%T.Pin returns unwrapped ErrNotSupported", link)
+	}
+
+	if opts.loadPinned == nil {
+		if !xerrors.Is(err, ErrNotSupported) {
+			t.Errorf("%T.Pin doesn't return ErrNotSupported: %s", link, err)
+		}
+	} else {
+		if err != nil {
+			t.Fatalf("Can't pin %T: %s", link, err)
+		}
+
+		link2, err := opts.loadPinned(path)
+		if err != nil {
+			t.Fatalf("Can't load pinned %T: %s", link, err)
+		}
+		link2.Close()
+
+		if reflect.TypeOf(link) != reflect.TypeOf(link2) {
+			t.Errorf("Loading a pinned %T returns a %T", link, link2)
+		}
+	}
+
+	if err := link.Update(opts.prog); err != nil {
+		t.Fatalf("%T.Update returns an error: %s", link, err)
+	}
+
+	func() {
+		// Panicking is OK
+		defer func() {
+			recover()
+		}()
+
+		if err := link.Update(nil); err == nil {
+			t.Fatalf("%T.Update accepts nil program", link)
+		}
+	}()
+
+	if err := link.Close(); err != nil {
+		t.Fatalf("%T.Close returns an error: %s", link, err)
+	}
+}
diff --git a/link/program_test.go b/link/program_test.go
new file mode 100644
index 0000000..f4c2ccc
--- /dev/null
+++ b/link/program_test.go
@@ -0,0 +1,58 @@
+package link
+
+import (
+	"testing"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/asm"
+	"github.com/cilium/ebpf/internal/testutils"
+)
+
+func TestProgramAlter(t *testing.T) {
+	testutils.SkipOnOldKernel(t, "4.13", "SkSKB type")
+
+	var err error
+	var prog *ebpf.Program
+	prog, err = ebpf.NewProgram(&ebpf.ProgramSpec{
+		Type: ebpf.SkSKB,
+		Instructions: asm.Instructions{
+			asm.LoadImm(asm.R0, 0, asm.DWord),
+			asm.Return(),
+		},
+		License: "MIT",
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer prog.Close()
+
+	var sockMap *ebpf.Map
+	sockMap, err = ebpf.NewMap(&ebpf.MapSpec{
+		Type:       ebpf.MapType(15), // BPF_MAP_TYPE_SOCKMAP
+		KeySize:    4,
+		ValueSize:  4,
+		MaxEntries: 2,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer sockMap.Close()
+
+	err = RawAttachProgram(RawAttachProgramOptions{
+		Target:  sockMap.FD(),
+		Program: prog,
+		Attach:  ebpf.AttachSkSKBStreamParser,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = RawDetachProgram(RawDetachProgramOptions{
+		Target:  sockMap.FD(),
+		Program: prog,
+		Attach:  ebpf.AttachSkSKBStreamParser,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/link/syscalls_test.go b/link/syscalls_test.go
new file mode 100644
index 0000000..aabad55
--- /dev/null
+++ b/link/syscalls_test.go
@@ -0,0 +1,15 @@
+package link
+
+import (
+	"testing"
+
+	"github.com/cilium/ebpf/internal/testutils"
+)
+
+func TestHaveProgAttach(t *testing.T) {
+	testutils.CheckFeatureTest(t, haveProgAttach)
+}
+
+func TestHaveBPFLink(t *testing.T) {
+	testutils.CheckFeatureTest(t, haveBPFLink)
+}
diff --git a/prog_test.go b/prog_test.go
index 04168fc..f896cc6 100644
--- a/prog_test.go
+++ b/prog_test.go
@@ -388,44 +388,6 @@ func TestProgramFromFD(t *testing.T) {
 	prog2.Close()
 }
 
-func TestProgramAlter(t *testing.T) {
-	testutils.SkipOnOldKernel(t, "4.13", "SkSKB type")
-
-	var err error
-	var prog *Program
-	prog, err = NewProgram(&ProgramSpec{
-		Type: SkSKB,
-		Instructions: asm.Instructions{
-			asm.LoadImm(asm.R0, 0, asm.DWord),
-			asm.Return(),
-		},
-		License: "MIT",
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer prog.Close()
-
-	var sockMap *Map
-	sockMap, err = NewMap(&MapSpec{
-		Type:       MapType(15), // BPF_MAP_TYPE_SOCKMAP
-		KeySize:    4,
-		ValueSize:  4,
-		MaxEntries: 2,
-	})
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer sockMap.Close()
-
-	if err := prog.Attach(sockMap.FD(), AttachSkSKBStreamParser, AttachFlags(0)); err != nil {
-		t.Fatal(err)
-	}
-	if err := prog.Detach(sockMap.FD(), AttachSkSKBStreamParser, AttachFlags(0)); err != nil {
-		t.Fatal(err)
-	}
-}
-
 func TestHaveProgTestRun(t *testing.T) {
 	testutils.CheckFeatureTest(t, haveProgTestRun)
 }