Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable sudo nerdctl run to expose ports to localhost #242

Merged
merged 6 commits into from
Sep 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 6 additions & 19 deletions cmd/lima-guestagent/daemon_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"net"
"net/http"
"os"
"path/filepath"
"time"

"github.com/gorilla/mux"
Expand All @@ -21,28 +20,21 @@ func newDaemonCommand() *cobra.Command {
Short: "run the daemon",
RunE: daemonAction,
}
daemonCommand.Flags().String("socket", socketDefaultValue(), "the unix socket to listen on")
daemonCommand.Flags().Duration("tick", 3*time.Second, "tick for polling events")
return daemonCommand
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably this ticking is too heavy for watching iptables. Instead we should wait for NETLINK_AUDIT events or some eBPF stuff, but I can work on that in a separate PR after merging this.


func daemonAction(cmd *cobra.Command, args []string) error {
socket, err := cmd.Flags().GetString("socket")
if err != nil {
return err
}
if socket == "" {
return errors.New("socket must be specified")
}
socket := "/run/lima-guestagent.sock"
tick, err := cmd.Flags().GetDuration("tick")
if err != nil {
return err
}
if tick == 0 {
return errors.New("tick must be specified")
}
if os.Geteuid() == 0 {
jandubois marked this conversation as resolved.
Show resolved Hide resolved
return errors.New("must not run as the root")
if os.Geteuid() != 0 {
return errors.New("must run as the root")
}
logrus.Infof("event tick: %v", tick)

Expand All @@ -69,14 +61,9 @@ func daemonAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
if err := os.Chmod(socket, 0777); err != nil {
return err
}
logrus.Infof("serving the guest agent on %q", socket)
return srv.Serve(l)
}

func socketDefaultValue() string {
if xrd := os.Getenv("XDG_RUNTIME_DIR"); xrd != "" {
return filepath.Join(xrd, "lima-guestagent.sock")
}
logrus.Warn("$XDG_RUNTIME_DIR is not set, cannot determine the socket name")
return ""
}
16 changes: 2 additions & 14 deletions cmd/lima-guestagent/install_systemd_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,7 @@ func installSystemdAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
unitPath, err := systemdUnitPath()
if err != nil {
return err
}
unitPath := "/etc/systemd/system/lima-guestagent.service"
if _, err := os.Stat(unitPath); !errors.Is(err, os.ErrNotExist) {
logrus.Infof("File %q already exists, overwriting", unitPath)
} else {
Expand All @@ -48,7 +45,7 @@ func installSystemdAction(cmd *cobra.Command, args []string) error {
{"enable", "--now", "lima-guestagent.service"},
}
for _, args := range argss {
cmd := exec.Command("systemctl", append([]string{"--user"}, args...)...)
cmd := exec.Command("systemctl", append([]string{"--system"}, args...)...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
logrus.Infof("Executing: %s", strings.Join(cmd.Args, " "))
Expand All @@ -60,15 +57,6 @@ func installSystemdAction(cmd *cobra.Command, args []string) error {
return nil
}

func systemdUnitPath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
unitPath := filepath.Join(configDir, "systemd/user/lima-guestagent.service")
return unitPath, nil
}

//go:embed lima-guestagent.TEMPLATE.service
var systemdUnitTemplate string

Expand Down
2 changes: 1 addition & 1 deletion cmd/lima-guestagent/lima-guestagent.TEMPLATE.service
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ Type=simple
Restart=on-failure

[Install]
WantedBy=default.target
WantedBy=multi-user.target
5 changes: 0 additions & 5 deletions cmd/lima-guestagent/main_linux.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package main

import (
"errors"
"os"
"strings"

"github.com/lima-vm/lima/pkg/version"
Expand All @@ -28,9 +26,6 @@ func newApp() *cobra.Command {
if debug {
logrus.SetLevel(logrus.DebugLevel)
}
if os.Geteuid() == 0 {
return errors.New("must not run as the root")
}
return nil
}
rootCmd.AddCommand(
Expand Down
2 changes: 1 addition & 1 deletion docs/internal.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ SSH:
- `ssh.sock`: SSH control master socket

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

Host agent:
- `ha.pid`: hostagent PID
Expand Down
15 changes: 5 additions & 10 deletions pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ install -m 755 "${LIMA_CIDATA_MNT}"/lima-guestagent /usr/local/bin/lima-guestage

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need to uninstall the previous version of the guest agent

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably need to uninstall the previous version of the guest agent

Just out of curiosity: why do we need to remove it? install should be able to overwrite the file. What am I missing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not overwritten because we switched away from non-root to root

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant we have to remove non-root systemd unit

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant we have to remove non-root systemd unit

Ok, thanks, because that is not what mattfarina@3aba8f3 is doing...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense. Just pushed a change to remove the legacy systemd configuration.

# Launch the guestagent service
if [ -f /etc/alpine-release ]; then
# Create directory for the lima-guestagent socket (normally done by systemd)
mkdir -p /run/user/"${LIMA_CIDATA_UID}"
gid=$(id -g "${LIMA_CIDATA_USER}")
chown "${LIMA_CIDATA_UID}:${gid}" /run/user/"${LIMA_CIDATA_UID}"
chmod 700 /run/user/"${LIMA_CIDATA_UID}"
# Install the openrc lima-guestagent service script
cat >/etc/init.d/lima-guestagent <<'EOF'
#!/sbin/openrc-run
Expand All @@ -30,18 +25,18 @@ supervisor=supervise-daemon
name="lima-guestagent"
description="Forward ports to the lima-hostagent"

export XDG_RUNTIME_DIR="/run/user/${LIMA_CIDATA_UID}"
command=/usr/local/bin/lima-guestagent
command_args="daemon"
command_background=true
command_user="${LIMA_CIDATA_USER}:${LIMA_CIDATA_USER}"
pidfile="${XDG_RUNTIME_DIR}/lima-guestagent.pid"
pidfile="/run/lima-guestagent.pid"
EOF
chmod 755 /etc/init.d/lima-guestagent

rc-update add lima-guestagent default
rc-service lima-guestagent start
else
until [ -e "/run/user/${LIMA_CIDATA_UID}/systemd/private" ]; do sleep 3; done
sudo -iu "${LIMA_CIDATA_USER}" "XDG_RUNTIME_DIR=/run/user/${LIMA_CIDATA_UID}" lima-guestagent install-systemd
# Remove legacy systemd service
rm -f "/home/${LIMA_CIDATA_USER}.linux/.config/systemd/user/lima-guestagent.service"

sudo lima-guestagent install-systemd
fi
23 changes: 23 additions & 0 deletions pkg/guestagent/guestagent_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/lima-vm/lima/pkg/guestagent/api"
"github.com/lima-vm/lima/pkg/guestagent/iptables"
"github.com/lima-vm/lima/pkg/guestagent/procnettcp"
"github.com/sirupsen/logrus"
"github.com/yalue/native_endian"
Expand Down Expand Up @@ -129,6 +130,28 @@ func (a *agent) LocalPorts(ctx context.Context) ([]api.IPPort, error) {
})
}
}

ipts, err := iptables.GetPorts()
if err != nil {
return res, err
}
for _, ipt := range ipts {
// Make sure the port isn't already listed from procnettcp
found := false
for _, re := range res {
if re.Port == ipt.Port {
found = true
}
}
if !found {
res = append(res,
api.IPPort{
IP: ipt.IP,
Port: ipt.Port,
})
}
}

return res, nil
}

Expand Down
141 changes: 141 additions & 0 deletions pkg/guestagent/iptables/iptables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package iptables

import (
"bytes"
"errors"
"net"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
)

type Entry struct {
TCP bool
IP net.IP
Port int
}

// This regex can detect a line in the iptables added by portmap to do the
// forwarding. The following two are examples of lines (notice that one has the
// destination IP and the other does not):
// -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
// -A CNI-DN-04579c7bb67f4c3f6cca0 -p tcp -m tcp --dport 8082 -j DNAT --to-destination 10.4.0.10:80
// The -A on the front is to amend the rule that was already created. portmap
// ensures the rule is created before creating this line so it is always -A.
// CNI-DN- is the prefix used for rule for an individual container.
// -d is followed by the IP address. The regular expression looks for a valid
// ipv4 IP address. We need to detect this IP.
// --dport is the destination port. We need to detect this port
// -j DNAT this tells us it's the line doing the port forwarding.
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`)

func GetPorts() ([]Entry, error) {
// TODO: add support for ipv6

// Detect the location of iptables. If it is not installed skip the lookup
// and return no results. The lookup is performed on each run so that the
// agent does not need to be started to detect if iptables was installed
// after the agent is already running.
pth, err := exec.LookPath("iptables")
if err != nil {
if errors.Is(err, exec.ErrNotFound) {
return nil, nil
}

return nil, err
}

res, err := listNATRules(pth)
if err != nil {
return nil, err
}

pts, err := parsePortsFromRules(res)
if err != nil {
return nil, err
}

return checkPortsOpen(pts)
}

func parsePortsFromRules(rules []string) ([]Entry, error) {
var entries []Entry
for _, rule := range rules {
if found := findPortRegex.FindStringSubmatch(rule); found != nil {
if len(found) == 4 {
port, err := strconv.Atoi(found[3])
if err != nil {
return nil, err
}

istcp := false
if found[2] == "tcp" {
istcp = true
}

// if the IP is blank the port forwarding the portforwarding,
// which gets information from this, will skip it. When no IP
// is present localhost will work.
ip := found[1]
if ip == "" {
ip = "127.0.0.1"
}
ent := Entry{
IP: net.ParseIP(ip),
Port: port,
TCP: istcp,
}
entries = append(entries, ent)
}
}
}

return entries, nil
}

// listNATRules performs the lookup with iptables and returns the raw rules
// Note, this does not use github.com/coreos/go-iptables (a transitive dependency
// of lima) because that package would require multiple calls to iptables. This
// function does everything in a single call.
func listNATRules(pth string) ([]string, error) {
args := []string{pth, "-t", "nat", "-S"}

var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Cmd{
Path: pth,
Args: args,
Stdout: &stdout,
Stderr: &stderr,
}
if err := cmd.Run(); err != nil {
return nil, err
}

// turn the output into a rule per line.
rules := strings.Split(stdout.String(), "\n")
if len(rules) > 0 && rules[len(rules)-1] == "" {
rules = rules[:len(rules)-1]
}

return rules, nil
}

func checkPortsOpen(pts []Entry) ([]Entry, error) {
var entries []Entry
for _, pt := range pts {
if pt.TCP {
conn, err := net.DialTimeout("tcp", net.JoinHostPort(pt.IP.String(), strconv.Itoa(pt.Port)), time.Second)
if err == nil && conn != nil {
conn.Close()
entries = append(entries, pt)
}
} else {
entries = append(entries, pt)
}
}

return entries, nil
}
Loading