Skip to content

Commit 605b027

Browse files
authored
Merge pull request #242 from mattfarina/fix-140
Enable sudo nerdctl run to expose ports to localhost
2 parents 06e35a6 + 2dbb2fd commit 605b027

File tree

11 files changed

+275
-53
lines changed

11 files changed

+275
-53
lines changed

cmd/lima-guestagent/daemon_linux.go

Lines changed: 6 additions & 19 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,28 +20,21 @@ 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")
36+
if os.Geteuid() != 0 {
37+
return errors.New("must run as the root")
4638
}
4739
logrus.Infof("event tick: %v", tick)
4840

@@ -69,14 +61,9 @@ func daemonAction(cmd *cobra.Command, args []string) error {
6961
if err != nil {
7062
return err
7163
}
64+
if err := os.Chmod(socket, 0777); err != nil {
65+
return err
66+
}
7267
logrus.Infof("serving the guest agent on %q", socket)
7368
return srv.Serve(l)
7469
}
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: 5 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,18 @@ 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+
# Remove legacy systemd service
39+
rm -f "/home/${LIMA_CIDATA_USER}.linux/.config/systemd/user/lima-guestagent.service"
40+
41+
sudo lima-guestagent install-systemd
4742
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: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package iptables
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"net"
7+
"os/exec"
8+
"regexp"
9+
"strconv"
10+
"strings"
11+
"time"
12+
)
13+
14+
type Entry struct {
15+
TCP bool
16+
IP net.IP
17+
Port int
18+
}
19+
20+
// This regex can detect a line in the iptables added by portmap to do the
21+
// forwarding. The following two are examples of lines (notice that one has the
22+
// destination IP and the other does not):
23+
// -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
24+
// -A CNI-DN-04579c7bb67f4c3f6cca0 -p tcp -m tcp --dport 8082 -j DNAT --to-destination 10.4.0.10:80
25+
// The -A on the front is to amend the rule that was already created. portmap
26+
// ensures the rule is created before creating this line so it is always -A.
27+
// CNI-DN- is the prefix used for rule for an individual container.
28+
// -d is followed by the IP address. The regular expression looks for a valid
29+
// ipv4 IP address. We need to detect this IP.
30+
// --dport is the destination port. We need to detect this port
31+
// -j DNAT this tells us it's the line doing the port forwarding.
32+
var findPortRegex = regexp.MustCompile(`-A\s+CNI-DN-\w*\s+(?:-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}))?(?:/32\s+)?-p (tcp)?.*--dport (\d+) -j DNAT`)
33+
34+
func GetPorts() ([]Entry, error) {
35+
// TODO: add support for ipv6
36+
37+
// Detect the location of iptables. If it is not installed skip the lookup
38+
// and return no results. The lookup is performed on each run so that the
39+
// agent does not need to be started to detect if iptables was installed
40+
// after the agent is already running.
41+
pth, err := exec.LookPath("iptables")
42+
if err != nil {
43+
if errors.Is(err, exec.ErrNotFound) {
44+
return nil, nil
45+
}
46+
47+
return nil, err
48+
}
49+
50+
res, err := listNATRules(pth)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
pts, err := parsePortsFromRules(res)
56+
if err != nil {
57+
return nil, err
58+
}
59+
60+
return checkPortsOpen(pts)
61+
}
62+
63+
func parsePortsFromRules(rules []string) ([]Entry, error) {
64+
var entries []Entry
65+
for _, rule := range rules {
66+
if found := findPortRegex.FindStringSubmatch(rule); found != nil {
67+
if len(found) == 4 {
68+
port, err := strconv.Atoi(found[3])
69+
if err != nil {
70+
return nil, err
71+
}
72+
73+
istcp := false
74+
if found[2] == "tcp" {
75+
istcp = true
76+
}
77+
78+
// if the IP is blank the port forwarding the portforwarding,
79+
// which gets information from this, will skip it. When no IP
80+
// is present localhost will work.
81+
ip := found[1]
82+
if ip == "" {
83+
ip = "127.0.0.1"
84+
}
85+
ent := Entry{
86+
IP: net.ParseIP(ip),
87+
Port: port,
88+
TCP: istcp,
89+
}
90+
entries = append(entries, ent)
91+
}
92+
}
93+
}
94+
95+
return entries, nil
96+
}
97+
98+
// listNATRules performs the lookup with iptables and returns the raw rules
99+
// Note, this does not use github.com/coreos/go-iptables (a transitive dependency
100+
// of lima) because that package would require multiple calls to iptables. This
101+
// function does everything in a single call.
102+
func listNATRules(pth string) ([]string, error) {
103+
args := []string{pth, "-t", "nat", "-S"}
104+
105+
var stdout bytes.Buffer
106+
var stderr bytes.Buffer
107+
cmd := exec.Cmd{
108+
Path: pth,
109+
Args: args,
110+
Stdout: &stdout,
111+
Stderr: &stderr,
112+
}
113+
if err := cmd.Run(); err != nil {
114+
return nil, err
115+
}
116+
117+
// turn the output into a rule per line.
118+
rules := strings.Split(stdout.String(), "\n")
119+
if len(rules) > 0 && rules[len(rules)-1] == "" {
120+
rules = rules[:len(rules)-1]
121+
}
122+
123+
return rules, nil
124+
}
125+
126+
func checkPortsOpen(pts []Entry) ([]Entry, error) {
127+
var entries []Entry
128+
for _, pt := range pts {
129+
if pt.TCP {
130+
conn, err := net.DialTimeout("tcp", net.JoinHostPort(pt.IP.String(), strconv.Itoa(pt.Port)), time.Second)
131+
if err == nil && conn != nil {
132+
conn.Close()
133+
entries = append(entries, pt)
134+
}
135+
} else {
136+
entries = append(entries, pt)
137+
}
138+
}
139+
140+
return entries, nil
141+
}

0 commit comments

Comments
 (0)