1// SPDX-License-Identifier: MIT
2
3package vm
4
5import (
6 "context"
7 "fmt"
8 "net/netip"
9 "strconv"
10 "strings"
11
12 "bnema.dev/ermite-runner/internal/cmdrun"
13)
14
15// tapPrefix must match the "ertap*" wildcard of the nftables policy.
16const tapPrefix = "ertap"
17
18func tapName(slot int) string { return tapPrefix + strconv.Itoa(slot) }
19
20// createTap creates a persistent TAP owned by uid (the user Firecracker runs
21// as, so it may open it), gives the host side its /30 address and brings it up.
22// It needs CAP_NET_ADMIN. On error the TAP is removed again.
23func createTap(ctx context.Context, run cmdrun.Runner, name string, uid int, gateway netip.Addr) error {
24 u := strconv.Itoa(uid)
25 if _, err := run.Run(ctx, nil, "ip", "tuntap", "add", "dev", name, "mode", "tap", "user", u, "group", u); err != nil {
26 return fmt.Errorf("create tap %s: %w", name, err)
27 }
28 for _, args := range [][]string{
29 {"addr", "add", gateway.String() + "/30", "dev", name},
30 {"link", "set", "dev", name, "up"},
31 } {
32 if _, err := run.Run(ctx, nil, "ip", args...); err != nil {
33 _ = deleteTap(context.WithoutCancel(ctx), run, name)
34 return fmt.Errorf("configure tap %s: %w", name, err)
35 }
36 }
37 return nil
38}
39
40// deleteTap removes a TAP; a missing device is not an error.
41func deleteTap(ctx context.Context, run cmdrun.Runner, name string) error {
42 out, err := run.Run(ctx, nil, "ip", "link", "del", "dev", name)
43 if err != nil && (strings.Contains(string(out), "Cannot find device") || strings.Contains(err.Error(), "Cannot find device")) {
44 return nil
45 }
46 return err
47}
48
49// leftoverTaps lists interfaces named like ours ("ertap<N>").
50func leftoverTaps(ctx context.Context, run cmdrun.Runner) ([]string, error) {
51 out, err := run.Run(ctx, nil, "ip", "-o", "link", "show")
52 if err != nil {
53 return nil, err
54 }
55 return parseTapNames(string(out)), nil
56}
57
58// parseTapNames extracts "ertap<digits>" interface names from `ip -o link show`
59// ("3: ertap0: <...>" or "3: ertap0@if2: <...>").
60func parseTapNames(s string) []string {
61 var out []string
62 for _, line := range strings.Split(s, "\n") {
63 _, rest, ok := strings.Cut(line, ": ")
64 if !ok {
65 continue
66 }
67 name, _, _ := strings.Cut(rest, ":")
68 name, _, _ = strings.Cut(name, "@")
69 if n, ok := strings.CutPrefix(name, tapPrefix); ok && n != "" {
70 if _, err := strconv.Atoi(n); err == nil {
71 out = append(out, name)
72 }
73 }
74 }
75 return out
76}