Skip to content

Commit

Permalink
Introduce unix socket proxying for Docker API clients
Browse files Browse the repository at this point in the history
Adds system unix-proxy command that creates a forwarding proxy over ssh connection definitions
Modifies podman machine start to automatically setup both rootless and rootful proxies, unless
--nocompat is specified (also adds --nocompat to init --now)
Modifies machine init to automatically create a docker.sock link unless --nolink is specified
Prints beginner usage information after machine start

Signed-off-by: Jason Greene <[email protected]>
  • Loading branch information
n1hility committed Sep 24, 2021
1 parent 7039862 commit 872a866
Show file tree
Hide file tree
Showing 8 changed files with 599 additions and 62 deletions.
15 changes: 14 additions & 1 deletion cmd/podman/machine/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var (
initOpts = machine.InitOptions{}
defaultMachineName = "podman-machine-default"
now bool
nocompat bool
)

func init() {
Expand Down Expand Up @@ -70,6 +71,18 @@ func init() {
"Start machine now",
)

flags.BoolVar(
&nocompat,
"nocompat", false,
"Disable compatibility socket proxies when starting",
)

flags.BoolVar(
&initOpts.NoLink,
"nolink", false,
"Do not link /var/run/docker.sock",
)

ImagePathFlagName := "image-path"
flags.StringVar(&initOpts.ImagePath, ImagePathFlagName, cfg.Engine.MachineImage, "Path to qcow image")
_ = initCmd.RegisterFlagCompletionFunc(ImagePathFlagName, completion.AutocompleteDefault)
Expand Down Expand Up @@ -105,7 +118,7 @@ func initMachine(cmd *cobra.Command, args []string) error {
return err
}
if now {
err = vm.Start(initOpts.Name, machine.StartOptions{})
err = vm.Start(initOpts.Name, machine.StartOptions{NoCompat: nocompat, NoLink: initOpts.NoLink})
if err == nil {
fmt.Printf("Machine %q started successfully\n", initOpts.Name)
}
Expand Down
12 changes: 7 additions & 5 deletions cmd/podman/machine/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
package machine

import (
"fmt"

"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/pkg/machine"
"github.com/containers/podman/v3/pkg/machine/qemu"
Expand All @@ -14,21 +12,26 @@ import (

var (
startCmd = &cobra.Command{
Use: "start [MACHINE]",
Use: "start [options] [MACHINE]",
Short: "Start an existing machine",
Long: "Start a managed virtual machine ",
RunE: start,
Args: cobra.MaximumNArgs(1),
Example: `podman machine start myvm`,
ValidArgsFunction: autocompleteMachine,
}

startOptions machine.StartOptions
)

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: startCmd,
Parent: machineCmd,
})
flags := startCmd.Flags()
flags.BoolVar(&startOptions.NoCompat, "nocompat", false, "Disable Docker API compatibility socket proxies")
flags.BoolVar(&startOptions.NoLink, "nolink", false, "Do not link /var/run/docker.sock",)
}

func start(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -60,9 +63,8 @@ func start(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
if err := vm.Start(vmName, machine.StartOptions{}); err != nil {
if err := vm.Start(vmName, startOptions); err != nil {
return err
}
fmt.Printf("Machine %q started successfully\n", vmName)
return nil
}
7 changes: 7 additions & 0 deletions cmd/podman/system/umask_noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// +build windows

package system

func umask(mask int) int {
return 0
}
9 changes: 9 additions & 0 deletions cmd/podman/system/umask_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// +build !windows

package system

import "syscall"

func umask(mask int) int {
return syscall.Umask(mask)
}
249 changes: 249 additions & 0 deletions cmd/podman/system/unixproxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
package system

import (
"fmt"
"io"
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"time"

"github.com/containers/common/pkg/completion"
"github.com/containers/podman/v3/cmd/podman/common"
"github.com/containers/podman/v3/cmd/podman/registry"
"github.com/containers/podman/v3/pkg/bindings"
"github.com/containers/podman/v3/pkg/rootless"
"github.com/containers/podman/v3/pkg/util"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
)

var (
unixProxyDescription = `Proxy a remote podman service
Proxies a remote podman service over a local unix domain socket.
`

upCmd = &cobra.Command{
Use: "unix-proxy [options] [URI]",
Args: cobra.MaximumNArgs(1),
Short: "Proxies a remote podman service over a local unix domain socket",
Long: unixProxyDescription,
RunE: proxy,
ValidArgsFunction: common.AutocompleteDefaultOneArg,
Example: `podman system unix-proxy unix:///tmp/podman.sock`,
}

upArgs = struct {
PidFile string
Quiet bool
}{}
)

type CloseWriteStream interface {
io.Reader
io.WriteCloser
CloseWrite() error
}

func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: upCmd,
Parent: systemCmd,
})

flags := upCmd.Flags()

flags.StringVarP(&upArgs.PidFile, "pid-file", "", "", "File to save PID")
_ = upCmd.RegisterFlagCompletionFunc("pid-file", completion.AutocompleteNone)
flags.BoolVarP(&upArgs.Quiet, "quiet", "q", false, "Suppress printed output")
_ = upCmd.RegisterFlagCompletionFunc("quiet", completion.AutocompleteNone)
}

func proxy(cmd *cobra.Command, args []string) error {
apiURI, err := resolveUnixURI(args)
if err != nil {
return err
}
logrus.Infof("using API endpoint: '%s'", apiURI)
// Clean up any old existing unix domain socket
var uri *url.URL
if len(apiURI) > 0 {
var err error
uri, err = url.Parse(apiURI)
if err != nil {
return err
}

// socket activation uses a unix:// socket in the shipped unit files but apiURI is coded as "" at this layer.
if uri.Scheme == "unix" {
if err := os.Remove(uri.Path); err != nil && !os.IsNotExist(err) {
return err
}
} else {
return errors.Errorf("Only unix domain sockets are supported as a proxy address: %s", uri)
}
}

if len(upArgs.PidFile) > 0 {
f, err := os.Create(upArgs.PidFile)
if err != nil {
errors.Wrap(err, "Error creating pid")
}
defer os.Remove(upArgs.PidFile)
pid := os.Getpid()
if _, err := f.WriteString(strconv.Itoa(pid)); err != nil {
errors.Wrap(err, "Error creating pid")
}
}

return setupProxy(uri)
}

func connectForward(bastion *bindings.Bastion) (CloseWriteStream, error) {
for retries := 1; ; retries++ {
forward, err := bastion.Client.Dial("unix", bastion.URI.Path)
if err == nil {
return forward.(ssh.Channel), nil
}
// Check if ssh connection is still alive
_, _, err2 := bastion.Client.Conn.SendRequest("alive@podman", true, nil)
if err2 != nil || retries > 2 {
// couldn't reconnect ssh tunnel, or the destination is unreachable
return nil, errors.Wrapf(err, "Couldn't reestablish ssh connection: %s", bastion.URI)
}

bastion.Reconnect()
}
}

func listenUnix(socketURI *url.URL) (net.Listener, error) {
oldmask := umask(0177)
defer umask(oldmask)
listener, err := net.Listen("unix", socketURI.Path)
if err != nil {
return listener, errors.Wrapf(err, "Error listening on socket: %s", socketURI.Path)
}

return listener, nil
}

func setupProxy(socketURI *url.URL) error {
cfg := registry.PodmanConfig()

uri, err := url.Parse(cfg.URI)
if err != nil {
return errors.Wrapf(err, "Not a valid url: %s", uri)
}

if uri.Scheme != "ssh" {
return errors.Errorf("Only ssh is supported, specify another connection: %s", uri)
}

bastion, err := bindings.CreateBastion(uri, "", cfg.Identity)
defer bastion.Client.Close()
if err != nil {
return err
}

printfOrQuiet("SSH Bastion connected: %s\n", uri)

listener, err := listenUnix(socketURI)
if err != nil {
return errors.Wrapf(err, "Error listening on socket: %s", socketURI.Path)
}
defer listener.Close()

printfOrQuiet("Listening on: %s\n", socketURI)

for {
acceptConnection(listener, &bastion, socketURI)
}
}

func printfOrQuiet(format string, a ...interface{}) (n int, err error) {
if !upArgs.Quiet {
return fmt.Printf(format, a...)
}

return 0, nil
}

func acceptConnection(listener net.Listener, bastion *bindings.Bastion, socketURI *url.URL) error {
con, err := accept(listener)
if err != nil {
return errors.Wrapf(err, "Error accepting on socket: %s", socketURI.Path)
}

src, ok := con.(CloseWriteStream)
if !ok {
con.Close()
return errors.Wrapf(err, "Underlying socket does not support half-close %s", socketURI.Path)
}

dest, err := connectForward(bastion)
if err != nil {
con.Close()
logrus.Error(err)
return nil // eat
}

go forward(src, dest)
go forward(dest, src)

return nil
}

func backOff(delay time.Duration) time.Duration {
if delay == 0 {
delay = 5 * time.Millisecond
} else {
delay *= 2
}
if delay > time.Second {
delay = time.Second
}
return delay
}

func accept(listener net.Listener) (net.Conn, error) {
con, err := listener.Accept()
delay := time.Duration(0)
if ne, ok := err.(net.Error); ok && ne.Temporary() {
delay = backOff(delay)
time.Sleep(delay)
}
return con, err
}

func forward(src io.Reader, dest CloseWriteStream) {
defer dest.CloseWrite()
io.Copy(dest, src)
}

func resolveUnixURI(_url []string) (string, error) {
socketName := "podman-remote.sock"

if len(_url) > 0 && _url[0] != "" {
return _url[0], nil
}

xdg, err := util.GetRuntimeDir()
if rootless.IsRootless() {
xdg = os.TempDir()
}

if err != nil {
return "", err
}

socketPath := filepath.Join(xdg, "podman", socketName)
if err := os.MkdirAll(filepath.Dir(socketPath), 0700); err != nil {
return "", err
}
return "unix:" + socketPath, nil
}
Loading

0 comments on commit 872a866

Please sign in to comment.