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

Implement a "proxy" device type (udp/tcp/unix-socket/unix-abstract between host and container) #2504

Closed
fwyzard opened this Issue Oct 15, 2016 · 42 comments

Comments

8 participants
@fwyzard

fwyzard commented Oct 15, 2016

For some use cases it may be useful to forward a UNIX socket from the host to the container.
One example is to share the SSH_AUTH_SOCK socket used by ssh-agent.

Currently I'm doing

      LXC_UID=`id -u`
      LXC_GID=`id -g`
      LXC_ROOTFS="/var/lib/lxd/containers/$CONTAINER/rootfs"
      LXC_SSH_AUTH_SOCK="/tmp/ssh-agent-$LXC_UID.socket"
      SOCKET=$LXC_ROOTFS$LXC_SSH_AUTH_SOCK
      if ! netstat -x -l | grep -q $SOCKET; then
          rm -f $SOCKET
          { socat UNIX-LISTEN:$SOCKET,fork,user=$LXC_UID,group=$LXC_GID,mode=600 UNIX-CONNECT:$SSH_AUTH_SOCK & } >& /dev/null
      fi

which relies on directly accessing the container's rootfs (and on the fact that my UID and GID are the same on the host and container).

I would propose support for this directly in lxd, e.g. via some syntax like

    lxc config device add <container> $LXC_SSH_AUTH_SOCK unix-socket path=$SSH_AUTH_SOCK
@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Oct 15, 2016

Member

You can bind mount individual files using the disk device type.

lxc config device add my-socket path=/container/path source=/hostpath

Until some remapping filesystem comes along, you'll still need your relay on the host to get around the ownership issue though.

Member

stgraber commented Oct 15, 2016

You can bind mount individual files using the disk device type.

lxc config device add my-socket path=/container/path source=/hostpath

Until some remapping filesystem comes along, you'll still need your relay on the host to get around the ownership issue though.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Oct 15, 2016

Member

Having native support in LXD for that would definitely be nice but it'd likely be pretty tricky to do due to how Unix sockets work.

The main issue being that LXD itself can restart at any time. And since Unix sockets aren't reconnectable, that'd mean breaking connectivity, requiring anything using the socket in the container to be restarted whenever LXD restarts.

Member

stgraber commented Oct 15, 2016

Having native support in LXD for that would definitely be nice but it'd likely be pretty tricky to do due to how Unix sockets work.

The main issue being that LXD itself can restart at any time. And since Unix sockets aren't reconnectable, that'd mean breaking connectivity, requiring anything using the socket in the container to be restarted whenever LXD restarts.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Oct 15, 2016

Member

The one way I can think of to deal with this would be to spawn a separate process for each of those socket proxies.

Member

stgraber commented Oct 15, 2016

The one way I can think of to deal with this would be to spawn a separate process for each of those socket proxies.

@stgraber stgraber changed the title from support forwarding UNIX sockets to an LXC/LXD container to Implement a "proxy" device type (udp/tcp/unix-socket/unix-abstract between host and container) Oct 20, 2016

@stgraber stgraber added this to the later milestone Oct 20, 2016

@stgraber stgraber referenced this issue Nov 23, 2016

Closed

WIP: Forward port from container to client #2416

4 of 25 tasks complete
@jsimonetti

This comment has been minimized.

Show comment
Hide comment
@jsimonetti

jsimonetti Nov 27, 2016

Contributor

Since a Unix socket is just an open file descriptor, you could pass All open files to a temporary process and after restart is complete, pass them back.

Im not sure this is possible with go, but with c you would call send_msg() with SCM_RIGHTS.

SCM_RIGHTS - Send or receive a set of open file descriptors from another process. The data portion contains an integer array of the file descriptors. The passed file descriptors behave as though they have been created with dup(2).

Contributor

jsimonetti commented Nov 27, 2016

Since a Unix socket is just an open file descriptor, you could pass All open files to a temporary process and after restart is complete, pass them back.

Im not sure this is possible with go, but with c you would call send_msg() with SCM_RIGHTS.

SCM_RIGHTS - Send or receive a set of open file descriptors from another process. The data portion contains an integer array of the file descriptors. The passed file descriptors behave as though they have been created with dup(2).

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 27, 2016

Member

You'd hit the same problem as when bind-mounting a unix socket though. If the socket is reset, its inode number changes, invalidating anything that's tied to it, including mounts and fds.

The only way to avoid this is to have a middleman that will always be running and which will reconnect as needed based on the path rather than a handle.

Member

stgraber commented Nov 27, 2016

You'd hit the same problem as when bind-mounting a unix socket though. If the socket is reset, its inode number changes, invalidating anything that's tied to it, including mounts and fds.

The only way to avoid this is to have a middleman that will always be running and which will reconnect as needed based on the path rather than a handle.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Jun 5, 2017

Member

For anyone interested, the currently planned design for this would be:

  • A new "proxy" device type in LXD
  • Each "proxy" device would get a matching proxy process running
  • That process will be the bridge between the host network namespace and the container's network namespace. First binding the host side, then switching context to the container and binding the other side there.
  • This will support proxying from regular udp/tcp sockets as well as unix sockets (including abstract ones).

Property wise, I expect it to be something like:

  • source ("tcp::", "udp::", "unix:/var/blah", "unix:@/var/blah")
  • destination ("tcp::", "udp::", "unix:/var/blah", "unix:@/var/blah")
  • reverse (if set to true, connect on the container side and bind on the host side)

The process would be multi-threaded, allowing for multiple simultaneous connections, it would also re-connect on connection failure.

Member

stgraber commented Jun 5, 2017

For anyone interested, the currently planned design for this would be:

  • A new "proxy" device type in LXD
  • Each "proxy" device would get a matching proxy process running
  • That process will be the bridge between the host network namespace and the container's network namespace. First binding the host side, then switching context to the container and binding the other side there.
  • This will support proxying from regular udp/tcp sockets as well as unix sockets (including abstract ones).

Property wise, I expect it to be something like:

  • source ("tcp::", "udp::", "unix:/var/blah", "unix:@/var/blah")
  • destination ("tcp::", "udp::", "unix:/var/blah", "unix:@/var/blah")
  • reverse (if set to true, connect on the container side and bind on the host side)

The process would be multi-threaded, allowing for multiple simultaneous connections, it would also re-connect on connection failure.

@stgraber stgraber referenced this issue Jun 18, 2017

Closed

LAN Access: Port Forwarding Plan #1363

0 of 3 tasks complete
@srkunze

This comment has been minimized.

Show comment
Hide comment
@srkunze

srkunze Jun 18, 2017

Contributor

What about performance when the complete traffic goes through another process?

Contributor

srkunze commented Jun 18, 2017

What about performance when the complete traffic goes through another process?

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Jun 18, 2017

Member

@srkunze you'd certainly take a bit of a CPU hit with this approach but would benefit from more flexibility (cross-protocol, no need for network connectivity in the container, host doesn't need to be the gateway, ...)

I also wouldn't be opposed to allowing for optimizations in that device type. Say adding a "proxytype" property (similar to nictype for nic device) which could take "relay" or "forward" as values. "relay" would use the intermediate process whereas "forward" would use iptables and would be restricted to same protocol.

Member

stgraber commented Jun 18, 2017

@srkunze you'd certainly take a bit of a CPU hit with this approach but would benefit from more flexibility (cross-protocol, no need for network connectivity in the container, host doesn't need to be the gateway, ...)

I also wouldn't be opposed to allowing for optimizations in that device type. Say adding a "proxytype" property (similar to nictype for nic device) which could take "relay" or "forward" as values. "relay" would use the intermediate process whereas "forward" would use iptables and would be restricted to same protocol.

@techtonik

This comment has been minimized.

Show comment
Hide comment
@techtonik

techtonik Jun 19, 2017

Contributor

How does it work with port forwarding?

  1. LXD client opens encrypted channel to host to start container and disconnects
  2. container starts, services bootstrap and start listening

Now what? I need to access container port from client. Which command should I execute to get to it?

Contributor

techtonik commented Jun 19, 2017

How does it work with port forwarding?

  1. LXD client opens encrypted channel to host to start container and disconnects
  2. container starts, services bootstrap and start listening

Now what? I need to access container port from client. Which command should I execute to get to it?

@techtonik

This comment has been minimized.

Show comment
Hide comment
@techtonik

techtonik Jul 23, 2017

Contributor

So, is there any progress on this issue?

Contributor

techtonik commented Jul 23, 2017

So, is there any progress on this issue?

@KishanRPatel

This comment has been minimized.

Show comment
Hide comment
@KishanRPatel

KishanRPatel Nov 14, 2017

Contributor

Just to clarify the overall design/logic for this feature at a very high level:

LXD is the daemon that will be spawning the multiple processes handling the port forwarding. Whenever a command is issued to setup this forwarding for a container, the daemon will spawn a "proxy" process to handle it and add its metadata to a new "proxy" device type data structure. It will also write this data to a config file to keep track of the processes handling the forwarding. In case of a reset, the daemon will read this file to handle the reconnect.

The logic for the processes that will be spawned:

The first thing to do would be binding the specified socket in the host namespace. Then you would switch namespaces to the correct container namespace and bind the specified socket in that namespace.

A few things that I am confused about:

How does the "bind, switch, bind" logic work for the "proxy" processes? Is it just binding, accepting, and listening in the host namespace and redirecting the data to the socket in the container namespace? Would this "middleman" process even need to do anything after it sets up this connection? I obviously am not an expert in sockets. I apologize if this is way off base.

What exactly are you supposed to do when LXD resets to reconnect? I am thinking that the "proxy" processes are the "middleman" and should not be affected by an LXD reset.

Contributor

KishanRPatel commented Nov 14, 2017

Just to clarify the overall design/logic for this feature at a very high level:

LXD is the daemon that will be spawning the multiple processes handling the port forwarding. Whenever a command is issued to setup this forwarding for a container, the daemon will spawn a "proxy" process to handle it and add its metadata to a new "proxy" device type data structure. It will also write this data to a config file to keep track of the processes handling the forwarding. In case of a reset, the daemon will read this file to handle the reconnect.

The logic for the processes that will be spawned:

The first thing to do would be binding the specified socket in the host namespace. Then you would switch namespaces to the correct container namespace and bind the specified socket in that namespace.

A few things that I am confused about:

How does the "bind, switch, bind" logic work for the "proxy" processes? Is it just binding, accepting, and listening in the host namespace and redirecting the data to the socket in the container namespace? Would this "middleman" process even need to do anything after it sets up this connection? I obviously am not an expert in sockets. I apologize if this is way off base.

What exactly are you supposed to do when LXD resets to reconnect? I am thinking that the "proxy" processes are the "middleman" and should not be affected by an LXD reset.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 14, 2017

Member

Correct, we're going to have one subprocess running for every proxy device that's setup.
We'll want those processes to survive a LXD daemon restart but have LXD be able to kill them when the container goes down. The easiest way to do this (and the way we do it with dnsmasq) is to simply write pid files on disk.

In general there will only be three times where those can be spawned or stopped:

  • When the container starts (all of them start)
  • When the container or one of its profiles change (some may start/stop)
  • When the container is stopped (all of them stop)

When LXD gets reset, nothing would need to happen to the running proxy processes for any of the containers. It just needs to be able to track them down should it need to stop them, so keeping a pidfile on disk is likely easiest.

As for the namespace logic, in general, you'll end up with one side of the proxy that's listening on a socket, possibly getting multiple connections to it. On the other side, the proxy is going to be creating a new connection every time it received one.

As we don't want to flip flop the proxy between namespaces for every connection, the easiest way to do this is to first have the proxy attach to whichever side it needs to listen on (either host or container), bind that socket, getting an fd back from the kernel, then switch namespace to the connecting side, at which point it can start accepting connections on the socket it bound earlier and open new connections to the target from the right namespace.

For every new connection on the bound socket, the proxy will need to establish a new connection to the target and then copy data back and forth between the two.

Member

stgraber commented Nov 14, 2017

Correct, we're going to have one subprocess running for every proxy device that's setup.
We'll want those processes to survive a LXD daemon restart but have LXD be able to kill them when the container goes down. The easiest way to do this (and the way we do it with dnsmasq) is to simply write pid files on disk.

In general there will only be three times where those can be spawned or stopped:

  • When the container starts (all of them start)
  • When the container or one of its profiles change (some may start/stop)
  • When the container is stopped (all of them stop)

When LXD gets reset, nothing would need to happen to the running proxy processes for any of the containers. It just needs to be able to track them down should it need to stop them, so keeping a pidfile on disk is likely easiest.

As for the namespace logic, in general, you'll end up with one side of the proxy that's listening on a socket, possibly getting multiple connections to it. On the other side, the proxy is going to be creating a new connection every time it received one.

As we don't want to flip flop the proxy between namespaces for every connection, the easiest way to do this is to first have the proxy attach to whichever side it needs to listen on (either host or container), bind that socket, getting an fd back from the kernel, then switch namespace to the connecting side, at which point it can start accepting connections on the socket it bound earlier and open new connections to the target from the right namespace.

For every new connection on the bound socket, the proxy will need to establish a new connection to the target and then copy data back and forth between the two.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 14, 2017

Member

One thing to keep in mind here as we're doing all this in Go is that you CANNOT use setns() after the Go runtime has started. That's because setns() applies to your current thread but Go has an internal scheduler which schedules goroutines among a set of threads. So calling setns() from Go may affect other goroutines and subsequent calls from your context may end up in another thread which wouldn't be in the right namespace anymore.

So you're going to need a trick to do the initial bind() and setns() piece of the logic above at a time where Go is guaranteed to be using only a single thread. Once that's done, you can start using normal goroutines to handle connections, establish new outside connections and to mirror data since no more namespace switch will be needed at that point.

Member

stgraber commented Nov 14, 2017

One thing to keep in mind here as we're doing all this in Go is that you CANNOT use setns() after the Go runtime has started. That's because setns() applies to your current thread but Go has an internal scheduler which schedules goroutines among a set of threads. So calling setns() from Go may affect other goroutines and subsequent calls from your context may end up in another thread which wouldn't be in the right namespace anymore.

So you're going to need a trick to do the initial bind() and setns() piece of the logic above at a time where Go is guaranteed to be using only a single thread. Once that's done, you can start using normal goroutines to handle connections, establish new outside connections and to mirror data since no more namespace switch will be needed at that point.

@KishanRPatel

This comment has been minimized.

Show comment
Hide comment
@KishanRPatel

KishanRPatel Nov 15, 2017

Contributor

I have a couple follow up questions after doing some research into switching namespaces in Go.

To switch namespaces in go safely, we found this trick called the CGO constructor trick. The trick basically runs C code before the Go runtime starts up to allow you to switch namespaces safely before the Go runtime starts. There is a file in this repo under /lxd/main_exec.go that seems to use this trick. For some more information about this method, you can check out this repo: CGO Trick.

The only problem with this trick is that you have to fork and exec in order to switch namespaces. That would mean that you would end up with a process in the host namespace and one in the container namespace. Through this method, the FD that was bound in one namespace under one process would not map to the same socket in another process in a different namespace. Because the FD is per process, it seems like this method would not work. Does this problem make sense, or is our understanding of file descriptors wrong?

One way that could work would be to have two processes that send the data between each other, but this seems like a horribly inefficient way to do this.

Another way could be to bind the socket and get the FD back in the CGO constructor code before the Go runtime has started and then that process would have the FD from the correct namespace while running in a different one. With this way though, would the FD work inside a different namespace?

Basically, we are very confused about how file descriptors would work across namespaces.

Contributor

KishanRPatel commented Nov 15, 2017

I have a couple follow up questions after doing some research into switching namespaces in Go.

To switch namespaces in go safely, we found this trick called the CGO constructor trick. The trick basically runs C code before the Go runtime starts up to allow you to switch namespaces safely before the Go runtime starts. There is a file in this repo under /lxd/main_exec.go that seems to use this trick. For some more information about this method, you can check out this repo: CGO Trick.

The only problem with this trick is that you have to fork and exec in order to switch namespaces. That would mean that you would end up with a process in the host namespace and one in the container namespace. Through this method, the FD that was bound in one namespace under one process would not map to the same socket in another process in a different namespace. Because the FD is per process, it seems like this method would not work. Does this problem make sense, or is our understanding of file descriptors wrong?

One way that could work would be to have two processes that send the data between each other, but this seems like a horribly inefficient way to do this.

Another way could be to bind the socket and get the FD back in the CGO constructor code before the Go runtime has started and then that process would have the FD from the correct namespace while running in a different one. With this way though, would the FD work inside a different namespace?

Basically, we are very confused about how file descriptors would work across namespaces.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 15, 2017

Member

I meant to play with this a bit tonight but the 2.20 release took too much time, I'll try to find some time for this tomorrow.

My current thought is that we should actually be able to do all this without requiring the use of much C code at all. The only bit we'll need to do from a C constructor is the setns itself, but everything else can be done in Go.

I'll try to write a small test binary which simply binds port 1234 on the host and has that forward to port 80 in the container. My thought on how to do this with the minimum amount of C is:

  • Spawn proxy passing it the target netns (/proc/PID/ns/net), host port and container port
  • The proxy will setup a minimal Go listener for port 1234 then re-exec itself with the fd number of that listener as an extra argument
  • The C constructor code will then detect that the argument is there and perform the setns
  • The go runtime will start as usual but now we're in the container's namespace and have an fd to the listening socket on the host, we can re-assemble a Go listener from that and start accepting connections on it.

If we want to do the reverse, binding a port in the container and having that result in connections coming out of the host namespace, we'd have the code do:

  • Spawn proxy passing it the source netns (/proc/PID/ns/net), host port and container port
  • The C constructor would detect that we don't have the extra fd argument and would setns to the container's namespace
  • The go could would get a basic listener
  • Then re-exec itself with that fd as an argument
  • The C constructor would detect the extra argument and skip the setns
  • We're back in Go in the host namespace with an fd to a socket in the container that we can start listening on

That effectively limits the use of the C constructor in the proxy to just basic argument parsing and calling setns. Everything else can be done in Go, limiting the amount of duplication needed between C and Go.

Member

stgraber commented Nov 15, 2017

I meant to play with this a bit tonight but the 2.20 release took too much time, I'll try to find some time for this tomorrow.

My current thought is that we should actually be able to do all this without requiring the use of much C code at all. The only bit we'll need to do from a C constructor is the setns itself, but everything else can be done in Go.

I'll try to write a small test binary which simply binds port 1234 on the host and has that forward to port 80 in the container. My thought on how to do this with the minimum amount of C is:

  • Spawn proxy passing it the target netns (/proc/PID/ns/net), host port and container port
  • The proxy will setup a minimal Go listener for port 1234 then re-exec itself with the fd number of that listener as an extra argument
  • The C constructor code will then detect that the argument is there and perform the setns
  • The go runtime will start as usual but now we're in the container's namespace and have an fd to the listening socket on the host, we can re-assemble a Go listener from that and start accepting connections on it.

If we want to do the reverse, binding a port in the container and having that result in connections coming out of the host namespace, we'd have the code do:

  • Spawn proxy passing it the source netns (/proc/PID/ns/net), host port and container port
  • The C constructor would detect that we don't have the extra fd argument and would setns to the container's namespace
  • The go could would get a basic listener
  • Then re-exec itself with that fd as an argument
  • The C constructor would detect the extra argument and skip the setns
  • We're back in Go in the host namespace with an fd to a socket in the container that we can start listening on

That effectively limits the use of the C constructor in the proxy to just basic argument parsing and calling setns. Everything else can be done in Go, limiting the amount of duplication needed between C and Go.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 16, 2017

Member

Going to spend a bit of time on the proof of concept this evening, that should give you a good base on top of which to implement all the other bits that are mentioned in this issue.

My current plan is to write a tiny piece of Go called "proxy" which will take the following arguments:

./proxy <listener pid> <listener address> <connector pid> <connector address>

To bind all addresses on port 1234 on the host and forward to 127.0.0.1 on port 80 of the container, one would use:

./proxy <pid of lxd> tcp:[::]:1234 <pid of container init> tcp:127.0.0.1:80

To bind 127.0.0.1:1234 in the container and have that connect to 1.2.3.4 on port 80 from the host, you'd do:

./proxy <pid of container init> tcp:127.0.0.1:1234 <pid of lxd> tcp:1.2.3.4:80

This should make for pretty clean code and then allow hooking up supports for udp sockets, unix sockets and abstract unix sockets. Once you've done that part, we'll look into hooking this up to LXD itself.

Member

stgraber commented Nov 16, 2017

Going to spend a bit of time on the proof of concept this evening, that should give you a good base on top of which to implement all the other bits that are mentioned in this issue.

My current plan is to write a tiny piece of Go called "proxy" which will take the following arguments:

./proxy <listener pid> <listener address> <connector pid> <connector address>

To bind all addresses on port 1234 on the host and forward to 127.0.0.1 on port 80 of the container, one would use:

./proxy <pid of lxd> tcp:[::]:1234 <pid of container init> tcp:127.0.0.1:80

To bind 127.0.0.1:1234 in the container and have that connect to 1.2.3.4 on port 80 from the host, you'd do:

./proxy <pid of container init> tcp:127.0.0.1:1234 <pid of lxd> tcp:1.2.3.4:80

This should make for pretty clean code and then allow hooking up supports for udp sockets, unix sockets and abstract unix sockets. Once you've done that part, we'll look into hooking this up to LXD itself.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 16, 2017

Member

main.go

package main

import (
	"fmt"
	"io"
	"net"
	"os"
	"strings"
	"syscall"

	"github.com/lxc/lxd/shared"
)

func main() {
	err := run()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	os.Exit(0)
}

func run() error {
	if len(os.Args) != 5 {
		return fmt.Errorf("Invalid arguments")
	}

	// Get all our arguments
	listenPid := os.Args[1]
	listenAddr := os.Args[2]
	connectPid := os.Args[3]
	connectAddr := os.Args[4]

	// Check where we are in initialization
	if !shared.PathExists("/proc/self/fd/100") {
		fmt.Printf("Listening on %s in %s, forwarding to %s from %s\n", listenAddr, listenPid, connectAddr, connectPid)
		fmt.Printf("Setting up the listener\n")
		fields := strings.SplitN(listenAddr, ":", 2)

		addr, err := net.ResolveTCPAddr(fields[0], fields[1])
		if err != nil {
			return fmt.Errorf("failed to resolve listener address: %v", err)
		}

		listener, err := net.ListenTCP(fields[0], addr)
		if err != nil {
			return fmt.Errorf("failed to setup listener: %v", err)
		}

		file, err := listener.File()
		if err != nil {
			return fmt.Errorf("failed to extra fd from listener: %v", err)
		}
		defer file.Close()

		fd := file.Fd()
		err = syscall.Dup3(int(fd), 100, 0)
		if err != nil {
			return fmt.Errorf("failed to duplicate the listener fd: %v", err)
		}

		fmt.Printf("Re-executing ourselves\n")
		err = syscall.Exec("/proc/self/exe", os.Args, []string{})
		if err != nil {
			return fmt.Errorf("failed to re-exec: %v", err)
		}
	}

	// Re-create listener from fd
	listenFile := os.NewFile(100, "listener")
	listener, err := net.FileListener(listenFile)
	if err != nil {
		return fmt.Errorf("failed to re-assemble listener: %v", err)
	}

	fmt.Printf("Starting to proxy\n")
	for {
		// Accept a new client
		srcConn, err := listener.Accept()
		if err != nil {
			fmt.Fprintf(os.Stderr, "error: Failed to accept new connection: %v\n", err)
			continue
		}

		// Connect to the target
		fields := strings.SplitN(connectAddr, ":", 2)
		dstConn, err := net.Dial("tcp", fields[1])
		if err != nil {
			fmt.Fprintf(os.Stderr, "error: Failed to connect to target: %v\n", err)
			srcConn.Close()
			continue
		}

		go io.Copy(srcConn, dstConn)
		go io.Copy(dstConn, srcConn)
	}

	return nil
}

nsexec.go

package main

/*
#define _GNU_SOURCE

#include <linux/limits.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>

#define CMDLINE_SIZE (8 * PATH_MAX)

#define ADVANCE_ARG_REQUIRED() \
	do { \
		while (*cur != 0) \
			cur++; \
		cur++; \
		if (size <= cur - buf) { \
			return; \
		} \
	} while(0)

int dosetns(int pid, char *nstype) {
	int mntns;
	char buf[PATH_MAX];

	sprintf(buf, "/proc/%d/ns/%s", pid, nstype);
	mntns = open(buf, O_RDONLY);
	if (mntns < 0) {
		return -1;
	}

	if (setns(mntns, 0) < 0) {
		close(mntns);
		return -1;
	}
	close(mntns);

	return 0;
}

__attribute__((constructor)) void init(void) {
	int cmdline, listen_pid, connect_pid;
	char buf[CMDLINE_SIZE];
	ssize_t size;
	char *cur;

	// Read the arguments
	cmdline = open("/proc/self/cmdline", O_RDONLY);
	if (cmdline < 0) {
		_exit(1);
	}

	memset(buf, 0, sizeof(buf));
	if ((size = read(cmdline, buf, sizeof(buf)-1)) < 0) {
		close(cmdline);
		_exit(1);
	}
	close(cmdline);

	cur = buf;

	// Get the arguments
	ADVANCE_ARG_REQUIRED();
	listen_pid = atoi(cur);
	ADVANCE_ARG_REQUIRED();
	ADVANCE_ARG_REQUIRED();
	connect_pid = atoi(cur);
	ADVANCE_ARG_REQUIRED();

	// Join the listener ns if not already setup
	if (access("/proc/self/fd/100", F_OK) < 0) {
		// Attach to the network namespace of the listener
		if (dosetns(listen_pid, "net") < 0) {
			fprintf(stderr, "Failed setns to listener network namespace: %s\n", strerror(errno));
			_exit(1);
		}
	} else {
		// Join the connector ns now
		if (dosetns(connect_pid, "net") < 0) {
			fprintf(stderr, "Failed setns to connector network namespace: %s\n", strerror(errno));
			_exit(1);
		}
	}

	// We're done, jump back to Go
}
*/
import "C"

This gives me:

stgraber@castiana:~/Desktop/proxy$ sudo ./proxy 7148 tcp:127.0.0.1:80 10513 tcp:127.0.0.1:1234
Listening on tcp:127.0.0.1:80 in 7148, forwarding to tcp:127.0.0.1:1234 from 10513
Setting up the listener
Re-executing ourselves
Starting to proxy
Member

stgraber commented Nov 16, 2017

main.go

package main

import (
	"fmt"
	"io"
	"net"
	"os"
	"strings"
	"syscall"

	"github.com/lxc/lxd/shared"
)

func main() {
	err := run()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}

	os.Exit(0)
}

func run() error {
	if len(os.Args) != 5 {
		return fmt.Errorf("Invalid arguments")
	}

	// Get all our arguments
	listenPid := os.Args[1]
	listenAddr := os.Args[2]
	connectPid := os.Args[3]
	connectAddr := os.Args[4]

	// Check where we are in initialization
	if !shared.PathExists("/proc/self/fd/100") {
		fmt.Printf("Listening on %s in %s, forwarding to %s from %s\n", listenAddr, listenPid, connectAddr, connectPid)
		fmt.Printf("Setting up the listener\n")
		fields := strings.SplitN(listenAddr, ":", 2)

		addr, err := net.ResolveTCPAddr(fields[0], fields[1])
		if err != nil {
			return fmt.Errorf("failed to resolve listener address: %v", err)
		}

		listener, err := net.ListenTCP(fields[0], addr)
		if err != nil {
			return fmt.Errorf("failed to setup listener: %v", err)
		}

		file, err := listener.File()
		if err != nil {
			return fmt.Errorf("failed to extra fd from listener: %v", err)
		}
		defer file.Close()

		fd := file.Fd()
		err = syscall.Dup3(int(fd), 100, 0)
		if err != nil {
			return fmt.Errorf("failed to duplicate the listener fd: %v", err)
		}

		fmt.Printf("Re-executing ourselves\n")
		err = syscall.Exec("/proc/self/exe", os.Args, []string{})
		if err != nil {
			return fmt.Errorf("failed to re-exec: %v", err)
		}
	}

	// Re-create listener from fd
	listenFile := os.NewFile(100, "listener")
	listener, err := net.FileListener(listenFile)
	if err != nil {
		return fmt.Errorf("failed to re-assemble listener: %v", err)
	}

	fmt.Printf("Starting to proxy\n")
	for {
		// Accept a new client
		srcConn, err := listener.Accept()
		if err != nil {
			fmt.Fprintf(os.Stderr, "error: Failed to accept new connection: %v\n", err)
			continue
		}

		// Connect to the target
		fields := strings.SplitN(connectAddr, ":", 2)
		dstConn, err := net.Dial("tcp", fields[1])
		if err != nil {
			fmt.Fprintf(os.Stderr, "error: Failed to connect to target: %v\n", err)
			srcConn.Close()
			continue
		}

		go io.Copy(srcConn, dstConn)
		go io.Copy(dstConn, srcConn)
	}

	return nil
}

nsexec.go

package main

/*
#define _GNU_SOURCE

#include <linux/limits.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>

#define CMDLINE_SIZE (8 * PATH_MAX)

#define ADVANCE_ARG_REQUIRED() \
	do { \
		while (*cur != 0) \
			cur++; \
		cur++; \
		if (size <= cur - buf) { \
			return; \
		} \
	} while(0)

int dosetns(int pid, char *nstype) {
	int mntns;
	char buf[PATH_MAX];

	sprintf(buf, "/proc/%d/ns/%s", pid, nstype);
	mntns = open(buf, O_RDONLY);
	if (mntns < 0) {
		return -1;
	}

	if (setns(mntns, 0) < 0) {
		close(mntns);
		return -1;
	}
	close(mntns);

	return 0;
}

__attribute__((constructor)) void init(void) {
	int cmdline, listen_pid, connect_pid;
	char buf[CMDLINE_SIZE];
	ssize_t size;
	char *cur;

	// Read the arguments
	cmdline = open("/proc/self/cmdline", O_RDONLY);
	if (cmdline < 0) {
		_exit(1);
	}

	memset(buf, 0, sizeof(buf));
	if ((size = read(cmdline, buf, sizeof(buf)-1)) < 0) {
		close(cmdline);
		_exit(1);
	}
	close(cmdline);

	cur = buf;

	// Get the arguments
	ADVANCE_ARG_REQUIRED();
	listen_pid = atoi(cur);
	ADVANCE_ARG_REQUIRED();
	ADVANCE_ARG_REQUIRED();
	connect_pid = atoi(cur);
	ADVANCE_ARG_REQUIRED();

	// Join the listener ns if not already setup
	if (access("/proc/self/fd/100", F_OK) < 0) {
		// Attach to the network namespace of the listener
		if (dosetns(listen_pid, "net") < 0) {
			fprintf(stderr, "Failed setns to listener network namespace: %s\n", strerror(errno));
			_exit(1);
		}
	} else {
		// Join the connector ns now
		if (dosetns(connect_pid, "net") < 0) {
			fprintf(stderr, "Failed setns to connector network namespace: %s\n", strerror(errno));
			_exit(1);
		}
	}

	// We're done, jump back to Go
}
*/
import "C"

This gives me:

stgraber@castiana:~/Desktop/proxy$ sudo ./proxy 7148 tcp:127.0.0.1:80 10513 tcp:127.0.0.1:1234
Listening on tcp:127.0.0.1:80 in 7148, forwarding to tcp:127.0.0.1:1234 from 10513
Setting up the listener
Re-executing ourselves
Starting to proxy
@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 16, 2017

Member

And forwarding does work, though the connection handling isn't quite there so closing the connection in the container will not terminate the connection with the target, there's certainly some more cleanup to do there and to expand for all the other protocols, but as a proof of concept this should work.

Member

stgraber commented Nov 16, 2017

And forwarding does work, though the connection handling isn't quite there so closing the connection in the container will not terminate the connection with the target, there's certainly some more cleanup to do there and to expand for all the other protocols, but as a proof of concept this should work.

@katiewasnothere

This comment has been minimized.

Show comment
Hide comment
@katiewasnothere

katiewasnothere Nov 19, 2017

What is the desired syntax for this command? You mention earlier using lxc device add .... but we want to see if this is still what is wanted.

katiewasnothere commented Nov 19, 2017

What is the desired syntax for this command? You mention earlier using lxc device add .... but we want to see if this is still what is wanted.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 20, 2017

Member

Yeah, it's going to be a new device type, called "proxy".
I expect it to look like:

lxc config device add CONTAINER-NAME http proxy listen=tcp://1.2.3.4:80 connect=tcp://127.0.0.1:80 bind=host

Which would setup a new listener on the host, binding 1.2.3.4:80 and forwarding any connection to that over to the container at 127.0.0.1:80.

To get the reverse effect, you can do:

lxc config device add CONTAINER-NAME http proxy listen=tcp://127.0.0.1:1234 connect=tcp://127.0.0.1:80 bind=container

Which would have the proxy listen on 127.0.0.1:1234 in the container and forward any connection on that to 127.0.0.1:80 on the host.

I'm open to suggestion on a better naming for that "bind" property as it may sound a bit obscure.

For the initial implementation we'd have a new device type called "proxy" with 3 properties:

  • listen (URL, string, required)
  • connect (URL, string, required)
  • mode (string, one of "host" or "container", defaults to "host")
Member

stgraber commented Nov 20, 2017

Yeah, it's going to be a new device type, called "proxy".
I expect it to look like:

lxc config device add CONTAINER-NAME http proxy listen=tcp://1.2.3.4:80 connect=tcp://127.0.0.1:80 bind=host

Which would setup a new listener on the host, binding 1.2.3.4:80 and forwarding any connection to that over to the container at 127.0.0.1:80.

To get the reverse effect, you can do:

lxc config device add CONTAINER-NAME http proxy listen=tcp://127.0.0.1:1234 connect=tcp://127.0.0.1:80 bind=container

Which would have the proxy listen on 127.0.0.1:1234 in the container and forward any connection on that to 127.0.0.1:80 on the host.

I'm open to suggestion on a better naming for that "bind" property as it may sound a bit obscure.

For the initial implementation we'd have a new device type called "proxy" with 3 properties:

  • listen (URL, string, required)
  • connect (URL, string, required)
  • mode (string, one of "host" or "container", defaults to "host")
@techtonik

This comment has been minimized.

Show comment
Hide comment
@techtonik

techtonik Nov 20, 2017

Contributor

Suggestion:

lxc config CONTAINER add proxy from=host:tcp://1.2.3.4:80 to=tcp://127.0.0.1:80
lxc config CONTAINER add proxy from=container:tcp://127.0.0.1:1234 to=tcp://127.0.0.1:80

I wonder if to= argument needs explicit host/container specification? Does anybody too need to forward port from container to the same container?

Contributor

techtonik commented Nov 20, 2017

Suggestion:

lxc config CONTAINER add proxy from=host:tcp://1.2.3.4:80 to=tcp://127.0.0.1:80
lxc config CONTAINER add proxy from=container:tcp://127.0.0.1:1234 to=tcp://127.0.0.1:80

I wonder if to= argument needs explicit host/container specification? Does anybody too need to forward port from container to the same container?

@fwyzard

This comment has been minimized.

Show comment
Hide comment
@fwyzard

fwyzard Nov 20, 2017

hi @stgraber,

I expect it to look like:

lxc config device add CONTAINER-NAME http proxy listen=tcp://1.2.3.4:80 connect=tcp://127.0.0.1:80 bind=host

why the http part of the command ?

fwyzard commented Nov 20, 2017

hi @stgraber,

I expect it to look like:

lxc config device add CONTAINER-NAME http proxy listen=tcp://1.2.3.4:80 connect=tcp://127.0.0.1:80 bind=host

why the http part of the command ?

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 20, 2017

Member

That's the identifier (device name) for that device in that container's config.

Member

stgraber commented Nov 20, 2017

That's the identifier (device name) for that device in that container's config.

@techtonik

This comment has been minimized.

Show comment
Hide comment
@techtonik

techtonik Nov 20, 2017

Contributor

Could it be made optional? I mean autogenerated with an option to set it explicitly.

Contributor

techtonik commented Nov 20, 2017

Could it be made optional? I mean autogenerated with an option to set it explicitly.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 20, 2017

Member

No, the list of devices attached to a container is a map so it's got to have a key.
All of LXD is working like that today, there's no point in changing things now.

Member

stgraber commented Nov 20, 2017

No, the list of devices attached to a container is a map so it's got to have a key.
All of LXD is working like that today, there's no point in changing things now.

@KishanRPatel

This comment has been minimized.

Show comment
Hide comment
@KishanRPatel

KishanRPatel Nov 28, 2017

Contributor

We have a few questions about the behavior of the proxy process in regards to LXD restarting, clients removing the proxy device, and connection failures.

If there is a connection error, do we want the proxy process to re-exec itself or just be killed? Our understanding is that we only want to remove the proxy device from the container's device list is if the client issues the remove device command.

When LXD restarts, do we want it to restart proxy processes that have died or just let them be dead? If we let them stay dead, then the client would have to manually restart the proxy with a new command.

Which container status codes should mean killing the proxy process(es) for the container? Should we only kill them when the container is Stopped?

Also, in our current design, we are going to create new files for each proxy device. The file will have the following naming convention: pid of container:name of proxy device:pid of the proxy process and will contain the args necessary to restart the proxy process.

Contributor

KishanRPatel commented Nov 28, 2017

We have a few questions about the behavior of the proxy process in regards to LXD restarting, clients removing the proxy device, and connection failures.

If there is a connection error, do we want the proxy process to re-exec itself or just be killed? Our understanding is that we only want to remove the proxy device from the container's device list is if the client issues the remove device command.

When LXD restarts, do we want it to restart proxy processes that have died or just let them be dead? If we let them stay dead, then the client would have to manually restart the proxy with a new command.

Which container status codes should mean killing the proxy process(es) for the container? Should we only kill them when the container is Stopped?

Also, in our current design, we are going to create new files for each proxy device. The file will have the following naming convention: pid of container:name of proxy device:pid of the proxy process and will contain the args necessary to restart the proxy process.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 29, 2017

Member

If the proxy fails to start (bind), then it should exit non-zero which will have LXD return an error to the user.

If the proxy fails to connect to its target upon receiving a client connection, then I'd expect it to log an error and disconnect the client, but not exit as there's technically nothing wrong with the proxy and re-execing wouldn't fix anything.

I also wouldn't expect the LXD daemon restarting to cause any interaction with the proxy processes. Those processes should be tied to the container lifecycle instead, so be spawned when the container starts and be killed when the container stops. If they somehow crash while running, then I'm fine with the user having to restart the container to get them back up.

So I don't think we actually need to store the args that were used, instead we only really need one file per proxy under /var/lib/lxd/devices/CONTAINER-NAME, that should be named after the name of the device entry in LXD and contain the PID.

For example for container "blah" with a proxy device called "http", I'd expect to see:

/var/lib/lxd/devices/blah/http.pid

Containing the PID of that particular proxy process.

Member

stgraber commented Nov 29, 2017

If the proxy fails to start (bind), then it should exit non-zero which will have LXD return an error to the user.

If the proxy fails to connect to its target upon receiving a client connection, then I'd expect it to log an error and disconnect the client, but not exit as there's technically nothing wrong with the proxy and re-execing wouldn't fix anything.

I also wouldn't expect the LXD daemon restarting to cause any interaction with the proxy processes. Those processes should be tied to the container lifecycle instead, so be spawned when the container starts and be killed when the container stops. If they somehow crash while running, then I'm fine with the user having to restart the container to get them back up.

So I don't think we actually need to store the args that were used, instead we only really need one file per proxy under /var/lib/lxd/devices/CONTAINER-NAME, that should be named after the name of the device entry in LXD and contain the PID.

For example for container "blah" with a proxy device called "http", I'd expect to see:

/var/lib/lxd/devices/blah/http.pid

Containing the PID of that particular proxy process.

@kianaalcala

This comment has been minimized.

Show comment
Hide comment
@kianaalcala

kianaalcala Nov 30, 2017

We are struggling to re-exec the proxy process. LXD prints out a usage error whenever we try to run syscall.Exec in the proxy process - it seems like it wants us to use run command but we can't do that because it will create a new process and we still want to use the same fd. We just want to re-exec, is there a way to do this or are we probably doing something wrong?

We are currently doing: syscall.Exec("/proc/self/exe", args.Params, []string{})

kianaalcala commented Nov 30, 2017

We are struggling to re-exec the proxy process. LXD prints out a usage error whenever we try to run syscall.Exec in the proxy process - it seems like it wants us to use run command but we can't do that because it will create a new process and we still want to use the same fd. We just want to re-exec, is there a way to do this or are we probably doing something wrong?

We are currently doing: syscall.Exec("/proc/self/exe", args.Params, []string{})

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 30, 2017

Member

So you're doing that re-exec from a sub-process of the main LXD right?

Member

stgraber commented Nov 30, 2017

So you're doing that re-exec from a sub-process of the main LXD right?

@kianaalcala

This comment has been minimized.

Show comment
Hide comment
@kianaalcala

kianaalcala commented Nov 30, 2017

Yes.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Nov 30, 2017

Member

I suspect the problem may be as simple as args.Params not including arg[0].

When calling syscall.Exec you should pass the exec path as first argument, the command name ("lxd") as second argument and then all actual arguments after that.

Member

stgraber commented Nov 30, 2017

I suspect the problem may be as simple as args.Params not including arg[0].

When calling syscall.Exec you should pass the exec path as first argument, the command name ("lxd") as second argument and then all actual arguments after that.

@KishanRPatel

This comment has been minimized.

Show comment
Hide comment
@KishanRPatel

KishanRPatel Dec 2, 2017

Contributor

I have a few questions about the proxy process:

You mentioned earlier that if the proxy fails to connect to its target upon receiving a client connection, we should log an error and disconnect the client, but not exit the process since nothing is wrong. Why would we not want to exit if there is a connection error on the target side? I would think that there would be no harm in exiting if we cannot connect to the target.

Currently, we are handling TCP and Unix connections, but not UDP connections for the proxy process since there is no way to get a listener for a UDP connection. In Go, net.ListenUDP() returns a connection and not a listener that we can turn into a file with a file descriptor. It seems like the only way to handle UDP then, would be to have to keep re-execing and switching namespaces back and forth every time we get a connection. This seems very inefficient though. Is there a better way to handle UDP?

Contributor

KishanRPatel commented Dec 2, 2017

I have a few questions about the proxy process:

You mentioned earlier that if the proxy fails to connect to its target upon receiving a client connection, we should log an error and disconnect the client, but not exit the process since nothing is wrong. Why would we not want to exit if there is a connection error on the target side? I would think that there would be no harm in exiting if we cannot connect to the target.

Currently, we are handling TCP and Unix connections, but not UDP connections for the proxy process since there is no way to get a listener for a UDP connection. In Go, net.ListenUDP() returns a connection and not a listener that we can turn into a file with a file descriptor. It seems like the only way to handle UDP then, would be to have to keep re-execing and switching namespaces back and forth every time we get a connection. This seems very inefficient though. Is there a better way to handle UDP?

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Dec 5, 2017

Member

I'll have to look into the UDP issue. At the kernel level, you sure can get a listener fd for udp so we just need to find the right way to have Go do that for us :)

Member

stgraber commented Dec 5, 2017

I'll have to look into the UDP issue. At the kernel level, you sure can get a listener fd for udp so we just need to find the right way to have Go do that for us :)

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Dec 5, 2017

Member

For the other question. Say you have the proxy forward from 127.0.0.1:1234 in the container to www.google.com:80. That means it's go a listening socket inside the container binding port 1234 (tcp) on 127.0.0.1.

Now a client connects to 127.0.0.1:1234 but the proxy fails to connect to www.google.com:80 because of a network glitch or because your laptop just doesn't have internet access at the time.

There is nothing wrong going on with the proxy, it just can't connect to the outside at that time. Having it exit and be respawned isn't going to make any difference and having it die completely (not respawned) would mean that the proxy will be broken even if host connectivity is restored and you'd then need to restart the container to fix it.

Instead I'd just expect an error be logged and the client to be disconnected. It would keep behaving in that way whenever it fails to connect. Once it can connect again, then things work as usual.

Member

stgraber commented Dec 5, 2017

For the other question. Say you have the proxy forward from 127.0.0.1:1234 in the container to www.google.com:80. That means it's go a listening socket inside the container binding port 1234 (tcp) on 127.0.0.1.

Now a client connects to 127.0.0.1:1234 but the proxy fails to connect to www.google.com:80 because of a network glitch or because your laptop just doesn't have internet access at the time.

There is nothing wrong going on with the proxy, it just can't connect to the outside at that time. Having it exit and be respawned isn't going to make any difference and having it die completely (not respawned) would mean that the proxy will be broken even if host connectivity is restored and you'd then need to restart the container to fix it.

Instead I'd just expect an error be logged and the client to be disconnected. It would keep behaving in that way whenever it fails to connect. Once it can connect again, then things work as usual.

@KishanRPatel

This comment has been minimized.

Show comment
Hide comment
@KishanRPatel

KishanRPatel Dec 7, 2017

Contributor

I have a question regarding the structure/organization of the proxy process. We currently have TCP implemented completely and are making good progress on Unix sockets.

While working on Unix sockets, we realized that we do not need to switch network namespaces for its implementation since Unix sockets are implemented using socket files, independent of the network namespace. Also, the Unix proxy needs the container name so that it can access its socket file in /var/lib/lxd/containers/CONTAINER_NAME/rootfs.

We are proposing to have 3 separate types of proxy processes based on the connection type necessary. The reasons we are proposing this split up:

  1. Different connection types require varying implementations that are difficult to generalize
  2. If we were to have one proxy process with different functions handling different connection types, the code would be harder to read and we would be passing it extraneous arguments that it would not need
  3. Unix connections do not even require a single namespace switch

Do you think it would be best to separate it into 3 separate processes started by different internal subcommands or should we just have a single process that would call 3 separate functions to handle each connection type?

Contributor

KishanRPatel commented Dec 7, 2017

I have a question regarding the structure/organization of the proxy process. We currently have TCP implemented completely and are making good progress on Unix sockets.

While working on Unix sockets, we realized that we do not need to switch network namespaces for its implementation since Unix sockets are implemented using socket files, independent of the network namespace. Also, the Unix proxy needs the container name so that it can access its socket file in /var/lib/lxd/containers/CONTAINER_NAME/rootfs.

We are proposing to have 3 separate types of proxy processes based on the connection type necessary. The reasons we are proposing this split up:

  1. Different connection types require varying implementations that are difficult to generalize
  2. If we were to have one proxy process with different functions handling different connection types, the code would be harder to read and we would be passing it extraneous arguments that it would not need
  3. Unix connections do not even require a single namespace switch

Do you think it would be best to separate it into 3 separate processes started by different internal subcommands or should we just have a single process that would call 3 separate functions to handle each connection type?

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Dec 7, 2017

Member

I think different functions for the different socket types is probably best.

While you indeed don't need to attach to the container's network namespace to get a unix socket going, you should attach to the container's user namespace and mount namespace so that the socket ownership is correct and so that you use the container's mount table.

Member

stgraber commented Dec 7, 2017

I think different functions for the different socket types is probably best.

While you indeed don't need to attach to the container's network namespace to get a unix socket going, you should attach to the container's user namespace and mount namespace so that the socket ownership is correct and so that you use the container's mount table.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Dec 7, 2017

Member

Also note that we may want to support proxying from tcp to unix socket or vice-versa, so having different functions for the different socket types that we can mix and match based on what's requested is certainly best.

Member

stgraber commented Dec 7, 2017

Also note that we may want to support proxying from tcp to unix socket or vice-versa, so having different functions for the different socket types that we can mix and match based on what's requested is certainly best.

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Dec 7, 2017

Member

I also looked into the UDP problem. The main issue there is that it requires a pretty different design that tcp and unix sockets as it's connection-less.

That effectively means that the fd you're getting should be loaded as a FileConn, then rather than doing accept() calls (which don't exist with udp), you should listen for new messages and for every message grab the peer connection, that should given you the IP and PORT of whoever sent you that message.

Then based on that you'll have to maintain a pool of UDP connections to forward things to the target.
This is needed so you can forward the right messages back and forth from multiple clients.

Member

stgraber commented Dec 7, 2017

I also looked into the UDP problem. The main issue there is that it requires a pretty different design that tcp and unix sockets as it's connection-less.

That effectively means that the fd you're getting should be loaded as a FileConn, then rather than doing accept() calls (which don't exist with udp), you should listen for new messages and for every message grab the peer connection, that should given you the IP and PORT of whoever sent you that message.

Then based on that you'll have to maintain a pool of UDP connections to forward things to the target.
This is needed so you can forward the right messages back and forth from multiple clients.

@katiewasnothere

This comment has been minimized.

Show comment
Hide comment
@katiewasnothere

katiewasnothere Dec 10, 2017

We plan on making separate PR's for TCP, unix, and UDP since we're creating separate functions for each. TCP should be pretty much ready for review, minus testing. Currently we've been manually testing, what would be the best way to write automated tests and ensure we haven't broken any other functionality?

katiewasnothere commented Dec 10, 2017

We plan on making separate PR's for TCP, unix, and UDP since we're creating separate functions for each. TCP should be pretty much ready for review, minus testing. Currently we've been manually testing, what would be the best way to write automated tests and ensure we haven't broken any other functionality?

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Dec 10, 2017

Member

For this kind of stuff, integration tests under tests/suite/... is usually best.

I'd just make sure that adding a proxy device then starting the container works and communication is functional. Then remove it from a running container, confirm that the proxy is gone and then add it back and confirm that adding to a live container works too.

This may be made slightly trickier than it sounds by the fact that we use a very minimal busybox container in our tests which may lack some of the commands you'd typically use for this.

Member

stgraber commented Dec 10, 2017

For this kind of stuff, integration tests under tests/suite/... is usually best.

I'd just make sure that adding a proxy device then starting the container works and communication is functional. Then remove it from a running container, confirm that the proxy is gone and then add it back and confirm that adding to a live container works too.

This may be made slightly trickier than it sounds by the fact that we use a very minimal busybox container in our tests which may lack some of the commands you'd typically use for this.

@KishanRPatel

This comment has been minimized.

Show comment
Hide comment
@KishanRPatel

KishanRPatel Dec 15, 2017

Contributor

So I have spent a lot of time(10+hours) trying to test the proxy device using busybox with no luck. I am testing using nc while redirecting the output to a file so I can check the output against the data that I sent through the proxy. The issue is that it seems like there is no way to background nc in busybox while redirecting its output to a file. I spent some time to see if could get it working with the Ubuntu:16.04 image, and after a lot of trial and error, I got a command that would work.

In Ubuntu, I used the following command:
lxc exec proxyTest -- nohup bash -c "nc -lkd 1234 > proxyTest.out &"

For Busybox, I have tried every conceivable permutation of the following command:
lxc exec proxyTest -- nohup sh -c "nc -l -p 1234 > proxyTest.out &"

I have a version of the test done that uses an Ubuntu image, but it's obviously much slower and not ideal for testing. Is there any other way to test the proxy using busybox without using nc that I am missing?

Contributor

KishanRPatel commented Dec 15, 2017

So I have spent a lot of time(10+hours) trying to test the proxy device using busybox with no luck. I am testing using nc while redirecting the output to a file so I can check the output against the data that I sent through the proxy. The issue is that it seems like there is no way to background nc in busybox while redirecting its output to a file. I spent some time to see if could get it working with the Ubuntu:16.04 image, and after a lot of trial and error, I got a command that would work.

In Ubuntu, I used the following command:
lxc exec proxyTest -- nohup bash -c "nc -lkd 1234 > proxyTest.out &"

For Busybox, I have tried every conceivable permutation of the following command:
lxc exec proxyTest -- nohup sh -c "nc -l -p 1234 > proxyTest.out &"

I have a version of the test done that uses an Ubuntu image, but it's obviously much slower and not ideal for testing. Is there any other way to test the proxy using busybox without using nc that I am missing?

@stgraber

This comment has been minimized.

Show comment
Hide comment
@stgraber

stgraber Dec 15, 2017

Member

Hmm, busybox's netcat really doesn't seem to like being backgrounded... that's frustrating.
An option would be to use nsenter to run the host's nc inside the container's network namespace:

nsenter -n -t $(lxc query /1.0/containers/proxyTest/state | jq .pid) -- nc -6 -l 1234
Member

stgraber commented Dec 15, 2017

Hmm, busybox's netcat really doesn't seem to like being backgrounded... that's frustrating.
An option would be to use nsenter to run the host's nc inside the container's network namespace:

nsenter -n -t $(lxc query /1.0/containers/proxyTest/state | jq .pid) -- nc -6 -l 1234

@stgraber stgraber modified the milestones: later, lxd-3.0 Jan 9, 2018

@stgraber stgraber closed this in 0babee0 Jan 9, 2018

@schkovich schkovich referenced this issue Jan 23, 2018

Closed

Added Proxy Device fails to start #4186

6 of 6 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment