Skip to content

Commit 45c9527

Browse files
authored
Merge pull request #4066 from AkihiroSuda/ebpf-tick
guestagent: ticker: watch sys_exit_bind with eBPF
2 parents 7e08d68 + 4962543 commit 45c9527

File tree

8 files changed

+234
-13
lines changed

8 files changed

+234
-13
lines changed

cmd/lima-guestagent/daemon_linux.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/lima-vm/lima/v2/pkg/guestagent"
1717
"github.com/lima-vm/lima/v2/pkg/guestagent/api/server"
1818
"github.com/lima-vm/lima/v2/pkg/guestagent/serialport"
19+
"github.com/lima-vm/lima/v2/pkg/guestagent/ticker"
1920
"github.com/lima-vm/lima/v2/pkg/portfwdserver"
2021
)
2122

@@ -52,17 +53,20 @@ func daemonAction(cmd *cobra.Command, _ []string) error {
5253
if os.Geteuid() != 0 {
5354
return errors.New("must run as the root user")
5455
}
55-
logrus.Infof("event tick: %v", tick)
5656

57-
newTicker := func() (<-chan time.Time, func()) {
58-
// TODO: use an equivalent of `bpftrace -e 'tracepoint:syscalls:sys_*_bind { printf("tick\n"); }')`,
59-
// without depending on `bpftrace` binary.
60-
// The agent binary will need CAP_BPF file cap.
61-
ticker := time.NewTicker(tick)
62-
return ticker.C, ticker.Stop
57+
logrus.Infof("event tick: %v", tick)
58+
simpleTicker := ticker.NewSimpleTicker(time.NewTicker(tick))
59+
tickerInst := simpleTicker
60+
// See /sys/kernel/debug/tracing/available_events for the list of available tracepoints
61+
tracepoints := []string{"syscalls:sys_exit_bind"}
62+
if ebpfTicker, err := ticker.NewEbpfTicker(tracepoints); err != nil {
63+
logrus.WithError(err).Warn("failed to create eBPF ticker, falling back to simple ticker")
64+
} else {
65+
logrus.Infof("using eBPF ticker with tracepoints: %v", tracepoints)
66+
tickerInst = ticker.NewCompoundTicker(simpleTicker, ebpfTicker)
6367
}
6468

65-
agent, err := guestagent.New(ctx, newTicker, tick*20)
69+
agent, err := guestagent.New(ctx, tickerInst, tick*20)
6670
if err != nil {
6771
return err
6872
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/apparentlymart/go-cidr v1.1.0
1212
github.com/balajiv113/fd v0.0.0-20230330094840-143eec500f3e
1313
github.com/cheggaaa/pb/v3 v3.1.7 // gomodjail:unconfined
14+
github.com/cilium/ebpf v0.19.0 // gomodjail:unconfined
1415
github.com/containerd/continuity v0.4.5
1516
github.com/containers/gvisor-tap-vsock v0.8.7 // gomodjail:unconfined
1617
github.com/coreos/go-semver v0.3.1

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMU
3434
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
3535
github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
3636
github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ=
37+
github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao=
38+
github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY=
3739
github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
3840
github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
3941
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
@@ -96,6 +98,8 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF
9698
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
9799
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
98100
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
101+
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s=
102+
github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
99103
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
100104
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
101105
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=

pkg/guestagent/guestagent_linux.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ import (
2121
"github.com/lima-vm/lima/v2/pkg/guestagent/iptables"
2222
"github.com/lima-vm/lima/v2/pkg/guestagent/kubernetesservice"
2323
"github.com/lima-vm/lima/v2/pkg/guestagent/procnettcp"
24+
"github.com/lima-vm/lima/v2/pkg/guestagent/ticker"
2425
"github.com/lima-vm/lima/v2/pkg/guestagent/timesync"
2526
)
2627

27-
func New(ctx context.Context, newTicker func() (<-chan time.Time, func()), iptablesIdle time.Duration) (Agent, error) {
28+
func New(ctx context.Context, ticker ticker.Ticker, iptablesIdle time.Duration) (Agent, error) {
2829
a := &agent{
29-
newTicker: newTicker,
30+
ticker: ticker,
3031
kubernetesServiceWatcher: kubernetesservice.NewServiceWatcher(),
3132
}
3233

@@ -95,7 +96,7 @@ type agent struct {
9596
// Ticker is like time.Ticker.
9697
// We can't use inotify for /proc/net/tcp, so we need this ticker to
9798
// reload /proc/net/tcp.
98-
newTicker func() (<-chan time.Time, func())
99+
ticker ticker.Ticker
99100

100101
worthCheckingIPTables bool
101102
worthCheckingIPTablesMu sync.RWMutex
@@ -197,8 +198,8 @@ func isEventEmpty(ev *api.Event) bool {
197198

198199
func (a *agent) Events(ctx context.Context, ch chan *api.Event) {
199200
defer close(ch)
200-
tickerCh, tickerClose := a.newTicker()
201-
defer tickerClose()
201+
tickerCh := a.ticker.Chan()
202+
defer a.ticker.Stop()
202203
var st eventState
203204
for {
204205
var ev *api.Event

pkg/guestagent/ticker/compound.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ticker
5+
6+
import (
7+
"time"
8+
)
9+
10+
func NewCompoundTicker(t1, t2 Ticker) Ticker {
11+
return &compoundTicker{t1, t2}
12+
}
13+
14+
type compoundTicker struct {
15+
t1, t2 Ticker
16+
}
17+
18+
func (ticker *compoundTicker) Chan() <-chan time.Time {
19+
ch := make(chan time.Time)
20+
go func() {
21+
defer ticker.Stop()
22+
for {
23+
select {
24+
case v, ok := <-ticker.t1.Chan():
25+
if !ok {
26+
return
27+
}
28+
ch <- v
29+
case v, ok := <-ticker.t2.Chan():
30+
if !ok {
31+
return
32+
}
33+
ch <- v
34+
}
35+
}
36+
}()
37+
return ch
38+
}
39+
40+
func (ticker *compoundTicker) Stop() {
41+
ticker.t1.Stop()
42+
ticker.t2.Stop()
43+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ticker
5+
6+
import (
7+
"strings"
8+
"time"
9+
10+
"github.com/cilium/ebpf"
11+
"github.com/cilium/ebpf/asm"
12+
"github.com/cilium/ebpf/link"
13+
"github.com/cilium/ebpf/ringbuf"
14+
"github.com/sirupsen/logrus"
15+
)
16+
17+
func NewEbpfTicker(tracepoints []string) (Ticker, error) {
18+
var (
19+
ticker ebpfTicker
20+
err error
21+
)
22+
ticker.events, err = ebpf.NewMap(&ebpf.MapSpec{
23+
Name: "lima_ticker_events",
24+
Type: ebpf.RingBuf,
25+
MaxEntries: 1 << 20,
26+
})
27+
if err != nil {
28+
ticker.Stop()
29+
return nil, err
30+
}
31+
32+
ticker.prog, err = buildEbpfProg(ticker.events)
33+
if err != nil {
34+
ticker.Stop()
35+
return nil, err
36+
}
37+
38+
for _, tp := range tracepoints {
39+
tpPair := strings.SplitN(tp, ":", 2)
40+
tpLink, err := link.Tracepoint(tpPair[0], tpPair[1], ticker.prog, nil)
41+
if err != nil {
42+
ticker.Stop()
43+
return nil, err
44+
}
45+
ticker.links = append(ticker.links, tpLink)
46+
}
47+
48+
ticker.reader, err = ringbuf.NewReader(ticker.events)
49+
if err != nil {
50+
ticker.Stop()
51+
return nil, err
52+
}
53+
54+
ticker.ch = make(chan time.Time)
55+
go func() {
56+
for {
57+
_, rdErr := ticker.reader.Read()
58+
if rdErr != nil {
59+
logrus.WithError(rdErr).Warn("ebpfTicker: failed to read ringbuf")
60+
ticker.Stop()
61+
return
62+
}
63+
ticker.ch <- time.Now()
64+
}
65+
}()
66+
67+
return &ticker, nil
68+
}
69+
70+
type ebpfTicker struct {
71+
events *ebpf.Map
72+
prog *ebpf.Program
73+
links []link.Link
74+
reader *ringbuf.Reader
75+
ch chan time.Time
76+
}
77+
78+
func (ticker *ebpfTicker) Chan() <-chan time.Time {
79+
return ticker.ch
80+
}
81+
82+
func (ticker *ebpfTicker) Stop() {
83+
if ticker.events != nil {
84+
_ = ticker.events.Close()
85+
}
86+
if ticker.prog != nil {
87+
_ = ticker.prog.Close()
88+
}
89+
for _, l := range ticker.links {
90+
_ = l.Close()
91+
}
92+
if ticker.reader != nil {
93+
_ = ticker.reader.Close()
94+
}
95+
if ticker.ch != nil {
96+
close(ticker.ch)
97+
}
98+
}
99+
100+
func buildEbpfProg(events *ebpf.Map) (*ebpf.Program, error) {
101+
inst := asm.Instructions{
102+
// ringbuf = &map
103+
asm.LoadMapPtr(asm.R1, events.FD()),
104+
105+
// data = FP - 8
106+
asm.Mov.Reg(asm.R2, asm.R10),
107+
asm.Add.Imm(asm.R2, -8),
108+
109+
// *data = 1
110+
asm.StoreImm(asm.R2, 0, 1, asm.Word),
111+
112+
// size = 1
113+
asm.Mov.Imm(asm.R3, 1),
114+
115+
// flags = 0
116+
asm.Mov.Imm(asm.R4, 0),
117+
118+
// long bpf_ringbuf_output(void *ringbuf, void *data, u64 size, u64 flags)
119+
// https://man7.org/linux/man-pages/man7/bpf-helpers.7.html
120+
asm.FnRingbufOutput.Call(),
121+
122+
// return 0
123+
asm.Mov.Imm(asm.R0, 0),
124+
asm.Return(),
125+
}
126+
127+
spec := &ebpf.ProgramSpec{
128+
Name: "lima_ticker",
129+
Type: ebpf.TracePoint,
130+
License: "Apache-2.0", // No need to be GPL?
131+
Instructions: inst,
132+
}
133+
134+
return ebpf.NewProgram(spec)
135+
}

pkg/guestagent/ticker/simple.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ticker
5+
6+
import (
7+
"time"
8+
)
9+
10+
func NewSimpleTicker(ticker *time.Ticker) Ticker {
11+
return &simpleTicker{Ticker: ticker}
12+
}
13+
14+
type simpleTicker struct {
15+
*time.Ticker
16+
}
17+
18+
func (ticker *simpleTicker) Chan() <-chan time.Time {
19+
return ticker.Ticker.C
20+
}

pkg/guestagent/ticker/ticker.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package ticker
5+
6+
import (
7+
"time"
8+
)
9+
10+
type Ticker interface {
11+
Chan() <-chan time.Time
12+
Stop()
13+
}

0 commit comments

Comments
 (0)