Skip to content

Commit b32e9b5

Browse files
committed
Enable sudo nerdctl run to expose ports to localhost
A couple changes were made to make this possible including: - Adding iptables package to read the ports exposed by portmap CNI plugin. This was done so that 1 call to iptables is all that is needed on each run. - Move the lima-guestagent to run as root. This is needed for permission to use iptables. Note, this implementation only includes ipv4 support. Does not work for ipv6. Signed-off-by: Matt Farina <[email protected]>
1 parent 06e35a6 commit b32e9b5

File tree

11 files changed

+241
-54
lines changed

11 files changed

+241
-54
lines changed

cmd/lima-guestagent/daemon_linux.go

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"net"
66
"net/http"
77
"os"
8-
"path/filepath"
98
"time"
109

1110
"github.com/gorilla/mux"
@@ -21,29 +20,19 @@ func newDaemonCommand() *cobra.Command {
2120
Short: "run the daemon",
2221
RunE: daemonAction,
2322
}
24-
daemonCommand.Flags().String("socket", socketDefaultValue(), "the unix socket to listen on")
2523
daemonCommand.Flags().Duration("tick", 3*time.Second, "tick for polling events")
2624
return daemonCommand
2725
}
2826

2927
func daemonAction(cmd *cobra.Command, args []string) error {
30-
socket, err := cmd.Flags().GetString("socket")
31-
if err != nil {
32-
return err
33-
}
34-
if socket == "" {
35-
return errors.New("socket must be specified")
36-
}
28+
socket := "/run/lima-guestagent.sock"
3729
tick, err := cmd.Flags().GetDuration("tick")
3830
if err != nil {
3931
return err
4032
}
4133
if tick == 0 {
4234
return errors.New("tick must be specified")
4335
}
44-
if os.Geteuid() == 0 {
45-
return errors.New("must not run as the root")
46-
}
4736
logrus.Infof("event tick: %v", tick)
4837

4938
newTicker := func() (<-chan time.Time, func()) {
@@ -69,14 +58,9 @@ func daemonAction(cmd *cobra.Command, args []string) error {
6958
if err != nil {
7059
return err
7160
}
61+
if err := os.Chmod(socket, 0777); err != nil {
62+
return err
63+
}
7264
logrus.Infof("serving the guest agent on %q", socket)
7365
return srv.Serve(l)
7466
}
75-
76-
func socketDefaultValue() string {
77-
if xrd := os.Getenv("XDG_RUNTIME_DIR"); xrd != "" {
78-
return filepath.Join(xrd, "lima-guestagent.sock")
79-
}
80-
logrus.Warn("$XDG_RUNTIME_DIR is not set, cannot determine the socket name")
81-
return ""
82-
}

cmd/lima-guestagent/install_systemd_linux.go

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ func installSystemdAction(cmd *cobra.Command, args []string) error {
2727
if err != nil {
2828
return err
2929
}
30-
unitPath, err := systemdUnitPath()
31-
if err != nil {
32-
return err
33-
}
30+
unitPath := "/etc/systemd/system/lima-guestagent.service"
3431
if _, err := os.Stat(unitPath); !errors.Is(err, os.ErrNotExist) {
3532
logrus.Infof("File %q already exists, overwriting", unitPath)
3633
} else {
@@ -48,7 +45,7 @@ func installSystemdAction(cmd *cobra.Command, args []string) error {
4845
{"enable", "--now", "lima-guestagent.service"},
4946
}
5047
for _, args := range argss {
51-
cmd := exec.Command("systemctl", append([]string{"--user"}, args...)...)
48+
cmd := exec.Command("systemctl", append([]string{"--system"}, args...)...)
5249
cmd.Stdout = os.Stdout
5350
cmd.Stderr = os.Stderr
5451
logrus.Infof("Executing: %s", strings.Join(cmd.Args, " "))
@@ -60,15 +57,6 @@ func installSystemdAction(cmd *cobra.Command, args []string) error {
6057
return nil
6158
}
6259

63-
func systemdUnitPath() (string, error) {
64-
configDir, err := os.UserConfigDir()
65-
if err != nil {
66-
return "", err
67-
}
68-
unitPath := filepath.Join(configDir, "systemd/user/lima-guestagent.service")
69-
return unitPath, nil
70-
}
71-
7260
//go:embed lima-guestagent.TEMPLATE.service
7361
var systemdUnitTemplate string
7462

cmd/lima-guestagent/lima-guestagent.TEMPLATE.service

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ Type=simple
77
Restart=on-failure
88

99
[Install]
10-
WantedBy=default.target
10+
WantedBy=multi-user.target

cmd/lima-guestagent/main_linux.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package main
22

33
import (
4-
"errors"
5-
"os"
64
"strings"
75

86
"github.com/lima-vm/lima/pkg/version"
@@ -28,9 +26,6 @@ func newApp() *cobra.Command {
2826
if debug {
2927
logrus.SetLevel(logrus.DebugLevel)
3028
}
31-
if os.Geteuid() == 0 {
32-
return errors.New("must not run as the root")
33-
}
3429
return nil
3530
}
3631
rootCmd.AddCommand(

docs/internal.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ SSH:
4646
- `ssh.sock`: SSH control master socket
4747

4848
Guest agent:
49-
- `ga.sock`: Forwarded to `/run/user/$UID/lima-guestagent.sock` in the guest, via SSH
49+
- `ga.sock`: Forwarded to `/run/lima-guestagent.sock` in the guest, via SSH
5050

5151
Host agent:
5252
- `ha.pid`: hostagent PID

pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ install -m 755 "${LIMA_CIDATA_MNT}"/lima-guestagent /usr/local/bin/lima-guestage
1717

1818
# Launch the guestagent service
1919
if [ -f /etc/alpine-release ]; then
20-
# Create directory for the lima-guestagent socket (normally done by systemd)
21-
mkdir -p /run/user/"${LIMA_CIDATA_UID}"
22-
gid=$(id -g "${LIMA_CIDATA_USER}")
23-
chown "${LIMA_CIDATA_UID}:${gid}" /run/user/"${LIMA_CIDATA_UID}"
24-
chmod 700 /run/user/"${LIMA_CIDATA_UID}"
2520
# Install the openrc lima-guestagent service script
2621
cat >/etc/init.d/lima-guestagent <<'EOF'
2722
#!/sbin/openrc-run
@@ -30,18 +25,16 @@ supervisor=supervise-daemon
3025
name="lima-guestagent"
3126
description="Forward ports to the lima-hostagent"
3227
33-
export XDG_RUNTIME_DIR="/run/user/${LIMA_CIDATA_UID}"
3428
command=/usr/local/bin/lima-guestagent
3529
command_args="daemon"
3630
command_background=true
37-
command_user="${LIMA_CIDATA_USER}:${LIMA_CIDATA_USER}"
38-
pidfile="${XDG_RUNTIME_DIR}/lima-guestagent.pid"
31+
pidfile="/run/lima-guestagent.pid"
3932
EOF
4033
chmod 755 /etc/init.d/lima-guestagent
4134

4235
rc-update add lima-guestagent default
4336
rc-service lima-guestagent start
4437
else
45-
until [ -e "/run/user/${LIMA_CIDATA_UID}/systemd/private" ]; do sleep 3; done
46-
sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" lima-guestagent install-systemd
38+
until [ -e "/run" ]; do sleep 3; done
39+
sudo lima-guestagent install-systemd
4740
fi

pkg/guestagent/guestagent_linux.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/lima-vm/lima/pkg/guestagent/api"
11+
"github.com/lima-vm/lima/pkg/guestagent/iptables"
1112
"github.com/lima-vm/lima/pkg/guestagent/procnettcp"
1213
"github.com/sirupsen/logrus"
1314
"github.com/yalue/native_endian"
@@ -129,6 +130,28 @@ func (a *agent) LocalPorts(ctx context.Context) ([]api.IPPort, error) {
129130
})
130131
}
131132
}
133+
134+
ipts, err := iptables.GetPorts()
135+
if err != nil {
136+
return res, err
137+
}
138+
for _, ipt := range ipts {
139+
// Make sure the port isn't already listed from procnettcp
140+
found := false
141+
for _, re := range res {
142+
if re.Port == ipt.Port {
143+
found = true
144+
}
145+
}
146+
if !found {
147+
res = append(res,
148+
api.IPPort{
149+
IP: ipt.IP,
150+
Port: ipt.Port,
151+
})
152+
}
153+
}
154+
132155
return res, nil
133156
}
134157

pkg/guestagent/iptables/iptables.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package iptables
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"net"
7+
"os/exec"
8+
"regexp"
9+
"strconv"
10+
"strings"
11+
)
12+
13+
type Entry struct {
14+
IP net.IP
15+
Port int
16+
}
17+
18+
// This regex can detect a line in the iptables added by portmap to do the
19+
// forwarding. The following two are examples of lines (notice that one has the
20+
// destinateion IP and the other does not):
21+
// -A CNI-DN-2e2f8d5b91929ef9fc152 -d 127.0.0.1/32 -p tcp -m tcp --dport 8081 -j DNAT --to-destination 10.4.0.7:80
22+
// -A CNI-DN-04579c7bb67f4c3f6cca0 -p tcp -m tcp --dport 8082 -j DNAT --to-destination 10.4.0.10:80
23+
// The -A on the front is to amend the rule that was already created. portmap
24+
// ensures the rule is created before creating this line so it is always -A.
25+
// CNI-DN- is the prefix used for rule for an individual container.
26+
// -d is followed by the IP address. The regular expression looks for a valid
27+
// ipv4 IP address. We need to detect this IP.
28+
// --dport is the destination port. We need to detect this port
29+
// -j DNAT this tells us it's the line doing the port forwarding.
30+
var findPortRegex = regexp.MustCompile(`.*-A\W+CNI-DN-\w*\W+(?:-d ((?:\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}))?(?:/\d+\W+)?-p.*--dport (\d+) -j DNAT`)
31+
32+
func GetPorts() ([]Entry, error) {
33+
// TODO: add support for ipv6
34+
35+
// Detect the location of iptables. If it is not installed skip the lookup
36+
// and return no results. The lookup is performed on each run so that the
37+
// agent does not need to be started to detect if iptables was installed
38+
// after the agent is already running.
39+
pth, err := exec.LookPath("iptables")
40+
if err != nil {
41+
if errors.Is(err, exec.ErrNotFound) {
42+
return nil, nil
43+
}
44+
45+
return nil, err
46+
}
47+
48+
res, err := listNATRules(pth)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
return parsePortsFromRules(res)
54+
}
55+
56+
func parsePortsFromRules(rules []string) ([]Entry, error) {
57+
var entries []Entry
58+
for _, rule := range rules {
59+
if found := findPortRegex.FindStringSubmatch(rule); found != nil {
60+
if len(found) == 3 {
61+
port, err := strconv.Atoi(found[2])
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
// if the IP is blank the port forwarding the portforwarding,
67+
// which gets information from this, will skip it. When no IP
68+
// is present localhost will work.
69+
ip := found[1]
70+
if ip == "" {
71+
ip = "127.0.0.1"
72+
}
73+
ent := Entry{
74+
IP: net.ParseIP(ip),
75+
Port: port,
76+
}
77+
entries = append(entries, ent)
78+
}
79+
}
80+
}
81+
82+
return entries, nil
83+
}
84+
85+
// listNATRules performs the lookup with iptables and returns the raw rules
86+
// Note, this does not use github.com/coreos/go-iptables (a transitive dependency
87+
// of lima) because that package would require multiple calls to iptables. This
88+
// function does everything in a single call.
89+
func listNATRules(pth string) ([]string, error) {
90+
args := []string{pth, "-t", "nat", "-S"}
91+
92+
var stdout bytes.Buffer
93+
var stderr bytes.Buffer
94+
cmd := exec.Cmd{
95+
Path: pth,
96+
Args: args,
97+
Stdout: &stdout,
98+
Stderr: &stderr,
99+
}
100+
if err := cmd.Run(); err != nil {
101+
return nil, err
102+
}
103+
104+
// turn the output into a rule per line.
105+
rules := strings.Split(stdout.String(), "\n")
106+
if len(rules) > 0 && rules[len(rules)-1] == "" {
107+
rules = rules[:len(rules)-1]
108+
}
109+
110+
return rules, nil
111+
}

0 commit comments

Comments
 (0)