diff --git a/examples/README.md b/examples/README.md
index 53fad305d..6021865c6 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -16,6 +16,8 @@
   Like kprobes, but with better performance and usability, for kernels 5.5 and later.
   * [tcp_connect](fentry/) - Trace outgoing IPv4 TCP connections.
   * [tcp_close](tcprtt/) - Log RTT of IPv4 TCP connections using eBPF CO-RE helpers.
+* XDP - Attach a program to a network interface to process incoming packets.
+  * [xdp](xdp/) - Print packet counts by IPv4 source address.
 * Add your use case(s) here!
 
 ## How to run
diff --git a/examples/headers/common.h b/examples/headers/common.h
index e333df33d..f68e532c5 100644
--- a/examples/headers/common.h
+++ b/examples/headers/common.h
@@ -56,6 +56,23 @@ enum bpf_map_type {
 	BPF_MAP_TYPE_INODE_STORAGE         = 28,
 };
 
+enum xdp_action {
+	XDP_ABORTED = 0,
+	XDP_DROP = 1,
+	XDP_PASS = 2,
+	XDP_TX = 3,
+	XDP_REDIRECT = 4,
+};
+
+struct xdp_md {
+	__u32 data;
+	__u32 data_end;
+	__u32 data_meta;
+	__u32 ingress_ifindex;
+	__u32 rx_queue_index;
+	__u32 egress_ifindex;
+};
+
 enum {
 	BPF_ANY     = 0,
 	BPF_NOEXIST = 1,
diff --git a/examples/xdp/bpf_bpfeb.go b/examples/xdp/bpf_bpfeb.go
new file mode 100644
index 000000000..543f476b6
--- /dev/null
+++ b/examples/xdp/bpf_bpfeb.go
@@ -0,0 +1,119 @@
+// Code generated by bpf2go; DO NOT EDIT.
+//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64
+// +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64
+
+package main
+
+import (
+	"bytes"
+	_ "embed"
+	"fmt"
+	"io"
+
+	"github.com/cilium/ebpf"
+)
+
+// loadBpf returns the embedded CollectionSpec for bpf.
+func loadBpf() (*ebpf.CollectionSpec, error) {
+	reader := bytes.NewReader(_BpfBytes)
+	spec, err := ebpf.LoadCollectionSpecFromReader(reader)
+	if err != nil {
+		return nil, fmt.Errorf("can't load bpf: %w", err)
+	}
+
+	return spec, err
+}
+
+// loadBpfObjects loads bpf and converts it into a struct.
+//
+// The following types are suitable as obj argument:
+//
+//     *bpfObjects
+//     *bpfPrograms
+//     *bpfMaps
+//
+// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
+func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
+	spec, err := loadBpf()
+	if err != nil {
+		return err
+	}
+
+	return spec.LoadAndAssign(obj, opts)
+}
+
+// bpfSpecs contains maps and programs before they are loaded into the kernel.
+//
+// It can be passed ebpf.CollectionSpec.Assign.
+type bpfSpecs struct {
+	bpfProgramSpecs
+	bpfMapSpecs
+}
+
+// bpfSpecs contains programs before they are loaded into the kernel.
+//
+// It can be passed ebpf.CollectionSpec.Assign.
+type bpfProgramSpecs struct {
+	XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"`
+}
+
+// bpfMapSpecs contains maps before they are loaded into the kernel.
+//
+// It can be passed ebpf.CollectionSpec.Assign.
+type bpfMapSpecs struct {
+	XdpStatsMap *ebpf.MapSpec `ebpf:"xdp_stats_map"`
+}
+
+// bpfObjects contains all objects after they have been loaded into the kernel.
+//
+// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
+type bpfObjects struct {
+	bpfPrograms
+	bpfMaps
+}
+
+func (o *bpfObjects) Close() error {
+	return _BpfClose(
+		&o.bpfPrograms,
+		&o.bpfMaps,
+	)
+}
+
+// bpfMaps contains all maps after they have been loaded into the kernel.
+//
+// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
+type bpfMaps struct {
+	XdpStatsMap *ebpf.Map `ebpf:"xdp_stats_map"`
+}
+
+func (m *bpfMaps) Close() error {
+	return _BpfClose(
+		m.XdpStatsMap,
+	)
+}
+
+// bpfPrograms contains all programs after they have been loaded into the kernel.
+//
+// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
+type bpfPrograms struct {
+	XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"`
+}
+
+func (p *bpfPrograms) Close() error {
+	return _BpfClose(
+		p.XdpProgFunc,
+	)
+}
+
+func _BpfClose(closers ...io.Closer) error {
+	for _, closer := range closers {
+		if err := closer.Close(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Do not access this directly.
+//go:embed bpf_bpfeb.o
+var _BpfBytes []byte
diff --git a/examples/xdp/bpf_bpfeb.o b/examples/xdp/bpf_bpfeb.o
new file mode 100644
index 000000000..db4205bc2
Binary files /dev/null and b/examples/xdp/bpf_bpfeb.o differ
diff --git a/examples/xdp/bpf_bpfel.go b/examples/xdp/bpf_bpfel.go
new file mode 100644
index 000000000..60d56a236
--- /dev/null
+++ b/examples/xdp/bpf_bpfel.go
@@ -0,0 +1,119 @@
+// Code generated by bpf2go; DO NOT EDIT.
+//go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64
+// +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64
+
+package main
+
+import (
+	"bytes"
+	_ "embed"
+	"fmt"
+	"io"
+
+	"github.com/cilium/ebpf"
+)
+
+// loadBpf returns the embedded CollectionSpec for bpf.
+func loadBpf() (*ebpf.CollectionSpec, error) {
+	reader := bytes.NewReader(_BpfBytes)
+	spec, err := ebpf.LoadCollectionSpecFromReader(reader)
+	if err != nil {
+		return nil, fmt.Errorf("can't load bpf: %w", err)
+	}
+
+	return spec, err
+}
+
+// loadBpfObjects loads bpf and converts it into a struct.
+//
+// The following types are suitable as obj argument:
+//
+//     *bpfObjects
+//     *bpfPrograms
+//     *bpfMaps
+//
+// See ebpf.CollectionSpec.LoadAndAssign documentation for details.
+func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error {
+	spec, err := loadBpf()
+	if err != nil {
+		return err
+	}
+
+	return spec.LoadAndAssign(obj, opts)
+}
+
+// bpfSpecs contains maps and programs before they are loaded into the kernel.
+//
+// It can be passed ebpf.CollectionSpec.Assign.
+type bpfSpecs struct {
+	bpfProgramSpecs
+	bpfMapSpecs
+}
+
+// bpfSpecs contains programs before they are loaded into the kernel.
+//
+// It can be passed ebpf.CollectionSpec.Assign.
+type bpfProgramSpecs struct {
+	XdpProgFunc *ebpf.ProgramSpec `ebpf:"xdp_prog_func"`
+}
+
+// bpfMapSpecs contains maps before they are loaded into the kernel.
+//
+// It can be passed ebpf.CollectionSpec.Assign.
+type bpfMapSpecs struct {
+	XdpStatsMap *ebpf.MapSpec `ebpf:"xdp_stats_map"`
+}
+
+// bpfObjects contains all objects after they have been loaded into the kernel.
+//
+// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
+type bpfObjects struct {
+	bpfPrograms
+	bpfMaps
+}
+
+func (o *bpfObjects) Close() error {
+	return _BpfClose(
+		&o.bpfPrograms,
+		&o.bpfMaps,
+	)
+}
+
+// bpfMaps contains all maps after they have been loaded into the kernel.
+//
+// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
+type bpfMaps struct {
+	XdpStatsMap *ebpf.Map `ebpf:"xdp_stats_map"`
+}
+
+func (m *bpfMaps) Close() error {
+	return _BpfClose(
+		m.XdpStatsMap,
+	)
+}
+
+// bpfPrograms contains all programs after they have been loaded into the kernel.
+//
+// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
+type bpfPrograms struct {
+	XdpProgFunc *ebpf.Program `ebpf:"xdp_prog_func"`
+}
+
+func (p *bpfPrograms) Close() error {
+	return _BpfClose(
+		p.XdpProgFunc,
+	)
+}
+
+func _BpfClose(closers ...io.Closer) error {
+	for _, closer := range closers {
+		if err := closer.Close(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Do not access this directly.
+//go:embed bpf_bpfel.o
+var _BpfBytes []byte
diff --git a/examples/xdp/bpf_bpfel.o b/examples/xdp/bpf_bpfel.o
new file mode 100644
index 000000000..056c49021
Binary files /dev/null and b/examples/xdp/bpf_bpfel.o differ
diff --git a/examples/xdp/main.go b/examples/xdp/main.go
new file mode 100644
index 000000000..d635fe1f5
--- /dev/null
+++ b/examples/xdp/main.go
@@ -0,0 +1,83 @@
+//go:build linux
+// +build linux
+
+// This program demonstrates attaching an eBPF program to a network interface
+// with XDP (eXpress Data Path). The program parses the IPv4 source address
+// from packets and writes the packet count by IP to an LRU hash map.
+// The userspace program (Go code in this file) prints the contents
+// of the map to stdout every second.
+// It is possible to modify the XDP program to drop or redirect packets
+// as well -- give it a try!
+package main
+
+import (
+	"fmt"
+	"log"
+	"net"
+	"os"
+	"strings"
+	"time"
+
+	"github.com/cilium/ebpf"
+	"github.com/cilium/ebpf/link"
+)
+
+// $BPF_CLANG and $BPF_CFLAGS are set by the Makefile.
+//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf xdp.c -- -I../headers
+
+func main() {
+	if len(os.Args) < 2 {
+		log.Fatalf("Please specify a network interface")
+	}
+
+	// Look up the network interface by name.
+	ifaceName := os.Args[1]
+	iface, err := net.InterfaceByName(ifaceName)
+	if err != nil {
+		log.Fatalf("lookup network iface %q: %s", ifaceName, err)
+	}
+
+	// Load pre-compiled programs into the kernel.
+	objs := bpfObjects{}
+	if err := loadBpfObjects(&objs, nil); err != nil {
+		log.Fatalf("loading objects: %s", err)
+	}
+	defer objs.Close()
+
+	// Attach the program.
+	l, err := link.AttachXDP(link.XDPOptions{
+		Program:   objs.XdpProgFunc,
+		Interface: iface.Index,
+	})
+	if err != nil {
+		log.Fatalf("could not attach XDP program: %s", err)
+	}
+	defer l.Close()
+
+	log.Printf("Attached XDP program to iface %q (index %d)", iface.Name, iface.Index)
+	log.Printf("Press Ctrl-C to exit and remove the program")
+
+	// Print the contents of the BPF hash map (source IP address -> packet count).
+	ticker := time.NewTicker(1 * time.Second)
+	defer ticker.Stop()
+	for range ticker.C {
+		s, err := formatMapContents(objs.XdpStatsMap)
+		if err != nil {
+			log.Printf("Error reading map: %s", err)
+			continue
+		}
+		log.Printf("Map contents:\n%s", s)
+	}
+}
+
+func formatMapContents(m *ebpf.Map) (string, error) {
+	var sb strings.Builder
+	var key, val uint32
+	iter := m.Iterate()
+	for iter.Next(&key, &val) {
+		sourceIP := net.IPv4(byte(key>>24), byte(key>>16), byte(key>>8), byte(key))
+		packetCount := val
+		sb.WriteString(fmt.Sprintf("\t%s => %d\n", sourceIP, packetCount))
+	}
+	return sb.String(), iter.Err()
+}
diff --git a/examples/xdp/xdp.c b/examples/xdp/xdp.c
new file mode 100644
index 000000000..0a41af50d
--- /dev/null
+++ b/examples/xdp/xdp.c
@@ -0,0 +1,76 @@
+// +build ignore
+
+#include <linux/if_ether.h>
+#include <linux/ip.h>
+#include "bpf_endian.h"
+#include "common.h"
+
+char __license[] SEC("license") = "Dual MIT/GPL";
+
+const int MAX_MAP_ENTRIES = 16;
+
+/* Define an LRU hash map for storing packet count by source IPv4 address */
+struct bpf_map_def SEC("maps") xdp_stats_map = {
+	.type        = BPF_MAP_TYPE_LRU_HASH,
+	.key_size    = sizeof(__u32), // source IPv4 address
+	.value_size  = sizeof(__u32), // packet count
+	.max_entries = MAX_MAP_ENTRIES,
+};
+
+/*
+Attempt to parse the IPv4 source address from the packet.
+Returns 0 if there is no IPv4 header field; otherwise returns non-zero.
+*/
+static __always_inline int parse_ip_src_addr(struct xdp_md *ctx, __u32 *ip_src_addr) {
+	void *data_end = (void *)(long)ctx->data_end;
+	void *data = (void *)(long)ctx->data;
+	void *pos = data;
+
+	// First, parse the ethernet header.
+	struct ethhdr *eth = pos;
+	int hdrsize = sizeof(*eth);
+	if (pos + hdrsize > data_end) {
+		return 0;
+	}
+
+	pos += hdrsize;
+	if (eth->h_proto != bpf_htons(ETH_P_IP)) {
+		// The protocol is not IPv4, so we can't parse an IPv4 source address.
+		return 0;
+	}
+
+	// Then parse the IP header.
+	struct iphdr *ip = pos;
+	hdrsize = sizeof(*ip);
+	if (pos + hdrsize > data_end) {
+		return 0;
+	}
+
+	// Return the source IP address in host byte order.
+	*ip_src_addr = (__u32)bpf_ntohl(ip->saddr);
+	return 1;
+}
+
+SEC("xdp_prog")
+int xdp_prog_func(struct xdp_md *ctx) {
+	__u32 ip;
+	if (!parse_ip_src_addr(ctx, &ip)) {
+		// Not an IPv4 packet, so don't count it.
+		goto done;
+	}
+
+	__u32 *pkt_count = bpf_map_lookup_elem(&xdp_stats_map, &ip);
+	if (!pkt_count) {
+		// No entry in the map for this IP address yet, so set the initial value to 1.
+		__u32 init_pkt_count = 1;
+		bpf_map_update_elem(&xdp_stats_map, &ip, &init_pkt_count, BPF_ANY);
+	} else {
+		// Entry already exists for this IP address,
+		// so increment it atomically using an LLVM built-in.
+		__sync_fetch_and_add(pkt_count, 1);
+	}
+
+	done:
+	// Try changing this to XDP_DROP and see what happens!
+	return XDP_PASS;
+}