[lxc-devel] [lxd/master] Added proxy device for forwarding tcp connections

jialin-li on Github lxc-bot at linuxcontainers.org
Sat Dec 16 03:23:47 UTC 2017


A non-text attachment was scrubbed...
Name: not available
Type: text/x-mailbox
Size: 402 bytes
Desc: not available
URL: <http://lists.linuxcontainers.org/pipermail/lxc-devel/attachments/20171216/fec9c892/attachment.bin>
-------------- next part --------------
From a76c9357cdee5ff2f59dbace594cb79a94298f70 Mon Sep 17 00:00:00 2001
From: KishanRPatel katiewasnothere dinopanda jialin-li kianaalcala
 <kishanrpatel at utexas.edu>
Date: Fri, 15 Dec 2017 21:21:15 -0600
Subject: [PATCH] Added proxy device for forwarding tcp connections

Signed-off-by: KishanRPatel katiewasnothere dinopanda jialin-li kianaalcala <kishanrpatel at utexas.edu>
---
 doc/containers.md         |  23 +++++++
 lxd/container.go          |  27 +++++++-
 lxd/container_lxc.go      | 154 +++++++++++++++++++++++++++++++++++++++++++
 lxd/daemon.go             |   1 -
 lxd/db/devices.go         |   4 ++
 lxd/main.go               |   3 +-
 lxd/main_args.go          |   2 +
 lxd/main_nsexec.go        |  37 +++++++++++
 lxd/main_proxy.go         | 165 ++++++++++++++++++++++++++++++++++++++++++++++
 lxd/proxy_device_utils.go |  93 ++++++++++++++++++++++++++
 shared/util.go            |  18 +++++
 test/suites/proxy.sh      |  78 ++++++++++++++++++++++
 12 files changed, 601 insertions(+), 4 deletions(-)
 create mode 100644 lxd/main_proxy.go
 create mode 100644 lxd/proxy_device_utils.go
 create mode 100644 test/suites/proxy.sh

diff --git a/doc/containers.md b/doc/containers.md
index 8d93f1f0a..3b918fcba 100644
--- a/doc/containers.md
+++ b/doc/containers.md
@@ -154,6 +154,7 @@ ID (database)   | Name                              | Description
 4               | [unix-block](#type-unix-block)    | Unix block device
 5               | [usb](#type-usb)                  | USB device
 6               | [gpu](#type-gpu)                  | GPU device
+7               | [proxy] (#type-proxy)             | Proxy device
 
 ### Type: none
 A none type device doesn't have any property and doesn't create anything inside the container.
@@ -318,6 +319,28 @@ uid         | int       | 0                 | no        | UID of the device owne
 gid         | int       | 0                 | no        | GID of the device owner in the container
 mode        | int       | 0660              | no        | Mode of the device in the container
 
+### Type: proxy
+Proxy devices are processes that forward data to/from ports in the container and
+ports in host. Used to communicate easily between two network namespaces.
+
+There will eventually be three supported connection types:
+ - `TCP`
+ - `UDP`
+ - `Unix`
+
+Current supported connection types:
+ - `TCP - TCP`
+
+Key         | Type      | Default           | Required  | Description                             
+:--         | :--       | :--               | :--       | :--                                     
+listen      | string    | -                 | yes       | the address and port to bind and listen
+connect     | string    | -                 | yes       | the address and port to connect to
+bind        | string    | -                 | yes       | which side to bind on (host/container)
+
+```
+lxc config device add <container_name> <device-name> proxy listen=<type>:<addr>:<port> listen=<type>:<addr>:<port> bind=<host/container>
+```
+
 ## Instance types
 LXD supports simple instance types. Those are represented as a string
 which can be passed at container creation time.
diff --git a/lxd/container.go b/lxd/container.go
index 4515d9c3a..92c9472ec 100644
--- a/lxd/container.go
+++ b/lxd/container.go
@@ -199,6 +199,17 @@ func containerValidDeviceConfigKey(t, k string) bool {
 		default:
 			return false
 		}
+	case "proxy":
+		switch k {
+		case "listen":
+			return true
+		case "connect":
+			return true
+		case "bind":
+			return true
+		default:
+			return false
+		}
 	case "none":
 		return false
 	default:
@@ -290,7 +301,7 @@ func containerValidDevices(db *db.Node, devices types.Devices, profile bool, exp
 			return fmt.Errorf("Missing device type for device '%s'", name)
 		}
 
-		if !shared.StringInSlice(m["type"], []string{"none", "nic", "disk", "unix-char", "unix-block", "usb", "gpu"}) {
+		if !shared.StringInSlice(m["type"], []string{"none", "nic", "disk", "unix-char", "unix-block", "usb", "gpu", "proxy"}) {
 			return fmt.Errorf("Invalid device type for device '%s'", name)
 		}
 
@@ -384,7 +395,19 @@ func containerValidDevices(db *db.Node, devices types.Devices, profile bool, exp
 		} else if m["type"] == "gpu" {
 			// Probably no checks needed, since we allow users to
 			// pass in all GPUs.
-		} else if m["type"] == "none" {
+		} else if m["type"] == "proxy" {
+			if m["listen"] == "" {
+				return fmt.Errorf("Proxy device entry is missing the required \"listen\" property.")
+			}
+
+			if m["connect"] == "" {
+				return fmt.Errorf("Proxy device entry is missing the required \"connect\" property.")				
+			}
+
+			if m["bind"] == "" {
+				return fmt.Errorf("Proxy device entry is missing the required \"bind\" property.")				
+			}
+ 		} else if m["type"] == "none" {
 			continue
 		} else {
 			return fmt.Errorf("Invalid device type: %s", m["type"])
diff --git a/lxd/container_lxc.go b/lxd/container_lxc.go
index f223fb702..32fbf7157 100644
--- a/lxd/container_lxc.go
+++ b/lxd/container_lxc.go
@@ -1844,6 +1844,7 @@ func (c *containerLXC) startCommon() (string, error) {
 	c.removeUnixDevices()
 	c.removeDiskDevices()
 	c.removeNetworkFilters()
+	c.removeProxyDevices()
 
 	var usbs []usbDevice
 	var gpus []gpuDevice
@@ -2234,6 +2235,8 @@ func (c *containerLXC) Start(stateful bool) error {
 		return err
 	}
 
+	c.restartProxyDevices()
+
 	logger.Info("Started container", ctxMap)
 
 	return nil
@@ -2553,6 +2556,12 @@ func (c *containerLXC) OnStop(target string) error {
 			logger.Error("Unable to remove network filters", log.Ctx{"container": c.Name(), "err": err})
 		}
 
+		// Clean all proxy devices
+		err = c.removeProxyDevices()
+		if err != nil {
+			logger.Error("Unable to remove proxy devices", log.Ctx{"container": c.Name(), "err": err})
+		}
+
 		// Reboot the container
 		if target == "reboot" {
 			// Start the container again
@@ -2916,6 +2925,7 @@ func (c *containerLXC) cleanup() {
 	c.removeUnixDevices()
 	c.removeDiskDevices()
 	c.removeNetworkFilters()
+	c.removeProxyDevices()
 
 	// Remove the security profiles
 	AADeleteProfile(c)
@@ -3950,6 +3960,11 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error {
 						}
 					}
 				}
+			} else if m["type"] == "proxy" {
+				err = c.removeProxyDevice(k)
+				if err != nil {
+					return err
+				}
 			}
 		}
 
@@ -4043,6 +4058,11 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error {
 					logger.Error(msg)
 					return fmt.Errorf(msg)
 				}
+			} else if m["type"] == "proxy" {
+				err = c.insertProxyDevice(k, m)
+				if err != nil {
+					return err
+				}
 			}
 		}
 
@@ -4071,6 +4091,11 @@ func (c *containerLXC) Update(args db.ContainerArgs, userRequested bool) error {
 						return err
 					}
 				}
+			} else if m["type"] == "proxy" {
+				err = c.updateProxyDevice(k, m)
+				if err != nil {
+					return err
+				}
 			}
 		}
 
@@ -5969,6 +5994,135 @@ func (c *containerLXC) removeUnixDevices() error {
 	return nil
 }
 
+func (c *containerLXC) insertProxyDevice(devName string, m types.Device) error {
+	if !c.IsRunning() {
+		return fmt.Errorf("Can't add proxy device to stopped container")
+	}
+
+	proxyValues, err := setupProxyProcInfo(c, m)
+	if err != nil {
+		return err
+	}
+
+	proxyPid, err := shared.RunCommandGetPid(
+											c.state.OS.ExecPath,
+											"proxydevstart",
+											proxyValues.listenPid,
+											proxyValues.listenAddr,
+											proxyValues.connectPid,
+											proxyValues.connectAddr,
+											"0")
+	if err != nil {
+		return fmt.Errorf("Error occurred when starting proxy device: %s", err)
+	}
+
+	err = createProxyDevInfoFile(c.DevicesPath(), devName, proxyPid)
+	if err != nil {
+		process, _ := os.FindProcess(proxyPid)
+		process.Kill()
+		return fmt.Errorf("Error occurred when writing metadata for proxy process: %s", err)
+	}
+
+	return nil
+}
+
+func (c *containerLXC) removeProxyDevice(devName string) error {
+	if !c.IsRunning() {
+		return fmt.Errorf("Can't remove proxy device from stopped container")
+	}
+
+	devFileName := fmt.Sprintf("proxy.%s", devName)
+	devPath := filepath.Join(c.DevicesPath(), devFileName)
+	err := killProxyProc(devPath)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (c *containerLXC) removeProxyDevices() error {
+	// Check that we actually have devices to remove
+	if !shared.PathExists(c.DevicesPath()) {
+		return nil
+	}
+
+	// Load the directory listing
+	devFiles, err := ioutil.ReadDir(c.DevicesPath())
+			if err != nil {
+		return err
+	}
+
+	for _, f := range devFiles {
+		// Skip non-proxy devices
+		if !strings.HasPrefix(f.Name(), "proxy.") {
+			continue
+			}
+
+		// Kill the process
+		devicePath := filepath.Join(c.DevicesPath(), f.Name())
+		err = killProxyProc(devicePath)
+		if err != nil {
+			logger.Error("failed removing proxy device", log.Ctx{"err": err, "path": devicePath})
+		}
+	}
+	
+	return nil
+}
+
+func (c *containerLXC) updateProxyDevice(devName string, m types.Device) error {
+	if !c.IsRunning() {
+		return fmt.Errorf("Can't update proxy device in stopped container")
+	}
+
+	fmt.Printf("updating the proxy device")
+	proxyValues, err := setupProxyProcInfo(c, m)
+	if err != nil {
+		return err
+	}
+
+	devFileName := fmt.Sprintf("proxy.%s", devName)
+	devPath := filepath.Join(c.DevicesPath(), devFileName)
+	err = killProxyProc(devPath)
+
+	if err != nil {
+		return fmt.Errorf("Error occurred when removing old proxy device")
+	}
+
+	proxyPid, err := shared.RunCommandGetPid(
+											c.state.OS.ExecPath,
+											"proxydevstart",
+											proxyValues.listenPid,
+											proxyValues.listenAddr,
+											proxyValues.connectPid,
+											proxyValues.connectAddr,
+											"0")
+	if err != nil {
+		return fmt.Errorf("Error occurred when starting new proxy device")
+	}
+
+	err = createProxyDevInfoFile(c.DevicesPath(), devName, proxyPid)
+	if err != nil {
+		process, _ := os.FindProcess(proxyPid)
+		process.Kill()
+		return fmt.Errorf("Error occurred when writing metadata for proxy process")
+	}
+
+	return nil
+}
+
+func (c *containerLXC) restartProxyDevices() {
+	for _, name := range c.expandedDevices.DeviceNames() {
+		m := c.expandedDevices[name]
+		if m["type"] == "proxy" {
+			err := c.insertProxyDevice(name, m)
+			if err != nil {
+				fmt.Printf("Error when starting proxy device '%s' for container %s: %s\n", name, c.name, err)
+			}
+		}
+	}
+}
+
 // Network device handling
 func (c *containerLXC) createNetworkDevice(name string, m types.Device) (string, error) {
 	var dev, n1 string
diff --git a/lxd/daemon.go b/lxd/daemon.go
index 3a22932cb..64a6246ce 100644
--- a/lxd/daemon.go
+++ b/lxd/daemon.go
@@ -436,7 +436,6 @@ func (d *Daemon) init() error {
 	if err != nil {
 		return fmt.Errorf("cannot start API endpoints: %v", err)
 	}
-
 	// Run the post initialization actions
 	err = d.Ready()
 	if err != nil {
diff --git a/lxd/db/devices.go b/lxd/db/devices.go
index 6911f4360..db4036e7f 100644
--- a/lxd/db/devices.go
+++ b/lxd/db/devices.go
@@ -25,6 +25,8 @@ func dbDeviceTypeToString(t int) (string, error) {
 		return "usb", nil
 	case 6:
 		return "gpu", nil
+	case 7:
+		return "proxy", nil
 	default:
 		return "", fmt.Errorf("Invalid device type %d", t)
 	}
@@ -46,6 +48,8 @@ func dbDeviceTypeToInt(t string) (int, error) {
 		return 5, nil
 	case "gpu":
 		return 6, nil
+	case "proxy":
+		return 7, nil
 	default:
 		return -1, fmt.Errorf("Invalid device type %s", t)
 	}
diff --git a/lxd/main.go b/lxd/main.go
index 35df502be..d702e3a13 100644
--- a/lxd/main.go
+++ b/lxd/main.go
@@ -45,7 +45,7 @@ func main() {
 // Index of SubCommand functions by command line name
 //
 // "forkputfile", "forkgetfile", "forkmount" and "forkumount" are handled specially in main_nsexec.go
-// "forkgetnet" is partially handled in nsexec.go (setns)
+// "forkgetnet" is partially handled in main_nsexec.go (setns)
 var subcommands = map[string]SubCommand{
 	// Main commands
 	"activateifneeded": cmdActivateIfNeeded,
@@ -65,4 +65,5 @@ var subcommands = map[string]SubCommand{
 	"forkexec":           cmdForkExec,
 	"netcat":             cmdNetcat,
 	"migratedumpsuccess": cmdMigrateDumpSuccess,
+	"proxydevstart":	  cmdProxyDevStart,
 }
diff --git a/lxd/main_args.go b/lxd/main_args.go
index 6a9db8a01..27ed25ee8 100644
--- a/lxd/main_args.go
+++ b/lxd/main_args.go
@@ -133,4 +133,6 @@ Internal commands (don't call these directly):
         Indicate that a migration dump was successful
     netcat
         Mirror a unix socket to stdin/stdout
+    proxydevstart
+        Start proxy device process for a container
 `
diff --git a/lxd/main_nsexec.go b/lxd/main_nsexec.go
index dde8b3d23..8ae4a0589 100644
--- a/lxd/main_nsexec.go
+++ b/lxd/main_nsexec.go
@@ -681,6 +681,41 @@ void forkgetnet(char *buf, char *cur, ssize_t size) {
 	// The rest happens in Go
 }
 
+void proxydevstart(char *buf, char *cur, ssize_t size) {
+	int cmdline, listen_pid, connect_pid, fdnum;
+
+	// Get the arguments
+	ADVANCE_ARG_REQUIRED();
+	listen_pid = atoi(cur);
+	ADVANCE_ARG_REQUIRED();
+	ADVANCE_ARG_REQUIRED();
+	connect_pid = atoi(cur);
+	ADVANCE_ARG_REQUIRED();
+	ADVANCE_ARG_REQUIRED();
+	fdnum = atoi(cur);
+
+	// Cannot pass through -1 to runCommand since it is interpreted as a flag
+	fdnum = fdnum == 0 ? -1 : fdnum;
+	
+	char fdpath[80];
+	sprintf(fdpath, "/proc/self/fd/%d", fdnum);
+
+	// Join the listener ns if not already setup
+	if (access(fdpath, 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);
+		}
+	}
+}
+
 __attribute__((constructor)) void init(void) {
 	int cmdline;
 	char buf[CMDLINE_SIZE];
@@ -723,6 +758,8 @@ __attribute__((constructor)) void init(void) {
 		forkumount(buf, cur, size);
 	} else if (strcmp(cur, "forkgetnet") == 0) {
 		forkgetnet(buf, cur, size);
+	} else if (strcmp(cur, "proxydevstart") == 0) {
+		proxydevstart(buf, cur, size);
 	}
 }
 */
diff --git a/lxd/main_proxy.go b/lxd/main_proxy.go
new file mode 100644
index 000000000..9cc2fb2d7
--- /dev/null
+++ b/lxd/main_proxy.go
@@ -0,0 +1,165 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"net"
+	"os"	
+	"os/signal"
+	"strings"
+	"strconv"
+	"syscall"	
+
+	"github.com/lxc/lxd/shared"
+)
+
+func cmdProxyDevStart(args *Args) error {
+	err := run(args)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "error: %v\n", err)
+		os.Exit(1)
+	}
+
+	os.Exit(0)
+	return nil
+}
+
+func run(args *Args) error {
+	if (len(args.Params) != 5) {
+		return fmt.Errorf("Invalid number of arguments")
+	}
+
+	// Get all our arguments
+	listenPid := args.Params[0]
+	listenAddr := args.Params[1]
+	connectPid := args.Params[2]
+	connectAddr := args.Params[3]
+
+	fd := -1
+	if args.Params[4] != "0" {
+		fd, _ = strconv.Atoi(args.Params[4])
+	}	
+	
+	// Check where we are in initialization
+	if !shared.PathExists(fmt.Sprintf("/proc/self/fd/%d", fd)) {
+		fmt.Fprintf(os.Stdout, "Listening on %s in %s, forwarding to %s from %s\n", listenAddr, listenPid, connectAddr, connectPid)
+
+		file, err := getListenerFile(listenAddr)
+		if err != nil {
+			return err
+		}
+		defer file.Close()
+
+		listenerFd := file.Fd()
+		if err != nil {
+			return fmt.Errorf("failed to duplicate the listener fd: %v", err)
+		}
+
+		newFd, _ := syscall.Dup(int(listenerFd))
+
+		fmt.Fprintf(os.Stdout, "Re-executing ourselves\n")
+
+		args.Params[4] = strconv.Itoa(int(newFd))
+		execArgs := append([]string{"lxd" ,"proxydevstart"}, args.Params...)
+
+		err = syscall.Exec("/proc/self/exe", execArgs, []string{})
+		if err != nil {
+			return fmt.Errorf("failed to re-exec: %v", err)
+		}
+	}
+
+	
+	// Re-create listener from fd
+	listenFile := os.NewFile(uintptr(fd), "listener")
+	listener, err := net.FileListener(listenFile)
+	if err != nil {
+		return fmt.Errorf("failed to re-assemble listener: %v", err)
+	}
+
+	defer listener.Close()
+
+	fmt.Fprintf(os.Stdout, "Starting to proxy\n")
+
+	// begin proxying
+	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
+		}
+		fmt.Printf("Accepted a new connection\n")
+		
+		// Connect to the target
+		dstConn, err := getDestConn(connectAddr)
+		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
+}
+
+func getListenerFile(listenAddr string) (os.File, error) {
+	fields := strings.SplitN(listenAddr, ":", 2)
+	addr := strings.Join(fields[1:], "")
+
+	listener, err := net.Listen(fields[0], addr)
+	if err != nil {
+		return os.File{}, err
+	}
+
+	file := &os.File{}
+	switch listener.(type) {
+	case *net.TCPListener:
+		tcpListener := listener.(*net.TCPListener)
+		file, err = tcpListener.File()
+	case *net.UnixListener:
+		unixListener := listener.(*net.UnixListener)
+		file, err = unixListener.File()
+	}
+
+	if err != nil {
+		return os.File{}, fmt.Errorf("Failed to get file from listener: %v\n", err)
+	}
+
+	return *file, nil
+}
+
+func getDestConn(connectAddr string) (net.Conn, error) {
+	fields := strings.SplitN(connectAddr, ":", 2)
+	addr := strings.Join(fields[1:], "")
+	return net.Dial(fields[0], addr)
+}
+
+func cleanupUnixSocket(listenAddr string) error {
+	fields := strings.SplitN(listenAddr, ":", 2)
+	addr := strings.Join(fields[1:], "")
+	if fields[0] == "unix" {
+		err := syscall.Unlink(addr)
+		return err
+	}
+	return nil
+}
+
+func handleSignal(listenAddr string) {
+	sigc := make(chan os.Signal, 1)
+	signal.Notify(sigc, os.Interrupt, syscall.SIGINT)
+	go func() {
+		// Wait for a SIGINT
+		sig := <-sigc
+		fmt.Fprintf(os.Stdout, "Caught signal %s: cleaning up...", sig)
+		err := cleanupUnixSocket(listenAddr)
+		if err != nil {
+			fmt.Fprintf(os.Stdout, "Error unlinking unix socket: %v", err)
+		}
+		// And we're done:
+		os.Exit(0)
+	}()
+}
+
diff --git a/lxd/proxy_device_utils.go b/lxd/proxy_device_utils.go
new file mode 100644
index 000000000..287c60666
--- /dev/null
+++ b/lxd/proxy_device_utils.go
@@ -0,0 +1,93 @@
+package main
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"syscall"
+)
+
+type proxyProcInfo struct {
+	listenPid		string
+	connectPid		string
+	connectAddr		string
+	listenAddr		string
+}
+
+func createProxyDevInfoFile(devicesPath string, proxyDev string, proxyPid int) error {
+	devFileName := fmt.Sprintf("proxy.%s", proxyDev)
+	filePath := filepath.Join(devicesPath, devFileName)
+	f, err := os.Create(filePath)
+
+	if err != nil {
+		return err 
+	}
+
+	defer f.Close()
+
+	info := fmt.Sprintf("%d", proxyPid)
+	_, err = f.WriteString(info)
+
+	return err
+}
+
+func setupProxyProcInfo(c container, device map[string]string) (*proxyProcInfo, error) {	
+	pid := c.InitPID()
+	containerPid := strconv.Itoa(int(pid))
+	lxdPid := strconv.Itoa(os.Getpid())
+
+	connectAddr := device["connect"]
+	listenAddr := device["listen"]
+
+	connectionType := strings.SplitN(connectAddr, ":", 2)[0]
+	listenerType := strings.SplitN(listenAddr, ":", 2)[0]
+
+	if connectionType != "tcp" {
+		return nil, fmt.Errorf("Proxy device currently doesnt support the connection type: %s", connectionType)
+	}
+	if listenerType != "tcp" {
+		return nil, fmt.Errorf("Proxy device currently doesnt support the listener type: %s", listenerType)
+	}
+
+	listenPid := "-1"
+	connectPid := "-1"
+
+	if (device["bind"] == "container") {
+		listenPid = containerPid
+		connectPid = lxdPid
+	} else if (device["bind"] == "host") {
+		listenPid = lxdPid
+		connectPid = containerPid
+	} else {
+		return nil, fmt.Errorf("No indicated binding side")
+	}
+
+	p := &proxyProcInfo{
+		listenPid:		listenPid,
+		connectPid:		connectPid,
+		connectAddr:	connectAddr,
+		listenAddr:		listenAddr,
+	}
+
+	return p, nil
+}
+
+func killProxyProc(devPath string) error {
+	contents, err := ioutil.ReadFile(devPath)
+	if err != nil {
+		return err
+	}
+
+	pid, _ := strconv.Atoi(string(contents))
+	if err != nil {
+		return err
+	}
+	
+	syscall.Kill(pid, syscall.SIGINT)
+	os.Remove(devPath)	
+	
+	return nil
+}
diff --git a/shared/util.go b/shared/util.go
index e84eaeb26..6ac886f4c 100644
--- a/shared/util.go
+++ b/shared/util.go
@@ -843,6 +843,24 @@ func RunCommand(name string, arg ...string) (string, error) {
 	return string(output), nil
 }
 
+func RunCommandGetPid(name string, arg ...string) (int, error) {
+	cmd := exec.Command(name, arg...)
+	cmd.Stdout = os.Stdout
+    cmd.Stderr = os.Stderr
+	err := cmd.Start()
+	go cmd.Wait()
+	if err != nil {
+		err := RunError{
+			msg: fmt.Sprintf("Failed to run: %s %s: %s", name, strings.Join(arg, " ")),
+			Err: err,
+		}
+		return -1, err
+	}
+	
+	processPid := cmd.Process.Pid
+	return processPid, nil
+}
+
 func TryRunCommand(name string, arg ...string) (string, error) {
 	var err error
 	var output string
diff --git a/test/suites/proxy.sh b/test/suites/proxy.sh
new file mode 100644
index 000000000..6472b5107
--- /dev/null
+++ b/test/suites/proxy.sh
@@ -0,0 +1,78 @@
+test_proxy_device() {
+  
+  MESSAGE="Proxy device test string"
+
+  lxc launch busybox proxyTester
+  lxc config device add proxyTester proxyDev proxy listen=tcp:127.0.0.1:1234 connect=tcp:127.0.0.1:4321 bind=host
+  if [ $(lxc config device list proxyTester) != "proxyDev" ]; then
+    echo "Proxy device was not added to container"
+  fi
+
+
+  lxc config device remove proxyTester proxyDev
+  if [ $(lxc config device list proxyTester) ]; then 
+    echo "Proxy device was not removed from container"
+  fi
+
+  lxc config device add proxyTester proxyDev proxy listen=tcp:127.0.0.1:1234 connect=tcp:127.0.0.1:4321 bind=host
+  lxc stop proxyTester
+  if [ $(lxc config device list proxyTester) != "proxyDev" ]; then
+    echo "Proxy device should not be deleted from config on container stop"
+  fi
+  lxc delete proxyTester
+
+  lxc launch busybox proxyTester
+  if [ $(lxc config device list proxyTester) ]; then 
+    echo "Proxy device was not deleted from config on container deletion"
+  fi
+
+
+  lxc config device add proxyTester proxyDev proxy listen=tcp:127.0.0.1:1234 connect=tcp:127.0.0.1:4321 bind=host
+  nsenter -n -t $(lxc query /1.0/containers/proxyTester/state | jq .pid) -- nc -6 -l 4321 > proxyTest.out &
+  sleep 2
+
+  exec 3>/dev/tcp/localhost/1234
+  echo ${MESSAGE} >&3
+  sleep 1
+
+  if [ "$(cat proxyTest.out)" != "${MESSAGE}" ]; then
+    echo "Proxy device did not properly send data from host to container"
+  fi
+
+  rm -f proxyTest.out
+
+  lxc restart proxyTester
+  if [ $(lxc config device list proxyTester) != "proxyDev" ]; then
+    echo "Proxy device should not be removed on container restart"
+  fi
+
+  nsenter -n -t $(lxc query /1.0/containers/proxyTester/state | jq .pid) -- nc -6 -l 4321 > proxyTest.out &
+  sleep 2
+
+  exec 3>/dev/tcp/localhost/1234
+  echo ${MESSAGE} >&3
+  sleep 1
+
+  if [ "$(cat proxyTest.out)" != "${MESSAGE}" ]; then
+    echo "Proxy device did not properly restart on container restart"
+  fi
+
+  rm -f proxyTest.out
+
+  lxc config device set proxyTester proxyDev connect tcp:127.0.0.1:1337
+  nsenter -n -t $(lxc query /1.0/containers/proxyTester/state | jq .pid) -- nc -6 -l 1337 > proxyTest.out &
+  sleep 2
+
+  exec 3>/dev/tcp/localhost/1234
+  echo ${MESSAGE} >&3
+  sleep 1
+
+  if [ "$(cat proxyTest.out)" != "${MESSAGE}" ]; then
+    echo "Proxy device did not properly restart when config was updated"
+  fi
+
+  rm -f proxyTest.out
+  lxc stop proxyTester
+  lxc delete proxyTester
+}
+


More information about the lxc-devel mailing list