Skip to content

Commit

Permalink
NodePort API spec & socat implementation
Browse files Browse the repository at this point in the history
The API can be used for exposing TCP/UDP ports in the network namespace
created along with the user namespace to the host network namespace.

* `api.sock` is created under the `rootlesskit --state-dir` directory
* API is described in `pkg/api/openapi.yaml`
 * `GET /v1/ports`
 * `POST /v1/ports`
 * `DELETE /v1/ports`
* `rootlessctl` CLI can be used as an API client:
 * `rootlessctl list-ports`
 * `rootlessctl add-ports`
 * `rootlessctl remove-ports`
* To expose child 80 as parent 8080: `rootlessctl --socket=/path/to/api.sock add-ports 0.0.0.0:8080:80/tcp`

Currently, only `socat` implementation is available as `rootlesskit
--port-driver`.

The code under `pkg/port/socat` package might be also reusable for other
projects.

Signed-off-by: Akihiro Suda <suda.akihiro@lab.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Oct 24, 2018
1 parent 2c13e77 commit e147262
Show file tree
Hide file tree
Showing 100 changed files with 9,870 additions and 1,189 deletions.
37 changes: 33 additions & 4 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 34 additions & 11 deletions README.md
Expand Up @@ -16,6 +16,7 @@ Kernel NAT using SUID-enabled [`lxc-user-nic(1)`](https://linuxcontainers.org/lx

```console
$ go get github.com/rootless-containers/rootlesskit/cmd/rootlesskit
$ go get github.com/rootless-containers/rootlesskit/cmd/rootlessctl
```

Requirements:
Expand Down Expand Up @@ -107,15 +108,17 @@ COMMANDS:
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
--debug debug mode
--state-dir value state directory
--net value host, vdeplug_slirp, vpnkit (default: "host")
--vpnkit-binary value path of VPNKit binary for --net=vpnkit (default: "vpnkit")
--mtu value MTU for non-host network (default: 65520 for slirp4netns, 1500 for others) (default: 0)
--copy-up value mount a filesystem and copy-up the contents. e.g. "--copy-up=/etc" (typically required for non-host network)
--copy-up-mode value tmpfs+symlink (default: "tmpfs+symlink")
--help, -h show help
--version, -v print the version
--debug debug mode
--state-dir value state directory
--net value network driver [host, slirp4netns, vpnkit, vdeplug_slirp] (default: "host")
--slirp4netns-binary value path of slirp4netns binary for --net=slirp4netns (default: "slirp4netns")
--vpnkit-binary value path of VPNKit binary for --net=vpnkit (default: "vpnkit")
--mtu value MTU for non-host network (default: 65520 for slirp4netns, 1500 for others) (default: 0)
--copy-up value mount a filesystem and copy-up the contents. e.g. "--copy-up=/etc" (typically required for non-host network)
--copy-up-mode value copy-up mode [tmpfs+symlink] (default: "tmpfs+symlink")
--port-driver value port driver for non-host network. [none, socat] (default: "none")
--help, -h show help
--version, -v print the version
```


Expand All @@ -124,6 +127,7 @@ GLOBAL OPTIONS:
The following files will be created in the `--state-dir` directory:
* `lock`: lock file
* `child_pid`: decimal PID text that can be used for `nsenter(1)`.
* `api.sock`: REST API socket for `rootlessctl`. See [Port forwarding](#port-forwarding) section.

Undocumented files are subject to change.

Expand All @@ -140,7 +144,7 @@ Currently there are three slirp implementations supported by rootlesskit:
Usage:

```console
$ rootlesskit --state=/run/user/1001/rootlesskit/foo --net=slirp4netns --copy-up=/etc bash
$ rootlesskit --state-dir=/run/user/1001/rootlesskit/foo --net=slirp4netns --copy-up=/etc bash
rootlesskit$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
Expand Down Expand Up @@ -176,7 +180,26 @@ Default network configuration for `--net=vpnkit`:
* Host: 192.168.65.2


Port forwarding:
### Port forwarding

`rootlessctl` can be used for exposing the ports in the network namespace to the host network namespace.
You also need to launch `rootlesskit` with `--port-driver=socat`.


For example, to expose 80 in the child as 8080 in the parent:

```console
$ rootlesskit --state-dir=/run/user/1001/rootlesskit/foo --net=slirp4netns --copy-up=/etc --port-driver=socat bash
rootlesskit$ rootlessctl --socket=/run/user/1001/rootlesskit/foo/api.sock add-ports 0.0.0.0:8080:80/tcp
1
rootlesskit$ rootlessctl --socket=/run/user/1001/rootlesskit/foo/api.sock list-ports
ID PROTO PARENTIP PARENTPORT CHILDPORT
1 tcp 0.0.0.0 8080 80
rootlesskit$ rootlessctl --socket=/run/user/1001/rootlesskit/foo/api.sock remove-ports 1
1
```

You can also expose the ports manually without using the API socket.
```console
$ pid=$(cat /run/user/1001/rootlesskit/foo/child_pid)
$ socat -t -- TCP-LISTEN:8080,reuseaddr,fork EXEC:"nsenter -U -n -t $pid socat -t -- STDIN TCP4\:127.0.0.1\:80"
Expand Down
57 changes: 57 additions & 0 deletions cmd/rootlessctl/main.go
@@ -0,0 +1,57 @@
package main

import (
"fmt"
"os"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"

"github.com/rootless-containers/rootlesskit/pkg/api/client"
)

func main() {
debug := false
app := cli.NewApp()
app.Name = "rootlessctl"
app.Usage = "RootlessKit API client"
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug",
Usage: "debug mode",
Destination: &debug,
},
cli.StringFlag{
Name: "socket",
Usage: "Path to api.sock (under the `rootlesskit --state-dir` directory)",
},
}
app.Commands = []cli.Command{
listPortsCommand,
addPortsCommand,
removePortsCommand,
}
app.Before = func(clicontext *cli.Context) error {
if debug {
logrus.SetLevel(logrus.DebugLevel)
}
return nil
}
if err := app.Run(os.Args); err != nil {
if debug {
fmt.Fprintf(os.Stderr, "error: %+v\n", err)
} else {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
}
os.Exit(1)
}
}

func newClient(clicontext *cli.Context) (client.Client, error) {
socketPath := clicontext.GlobalString("socket")
if socketPath == "" {
return nil, errors.New("please specify --socket")
}
return client.New(socketPath)
}
150 changes: 150 additions & 0 deletions cmd/rootlessctl/port.go
@@ -0,0 +1,150 @@
package main

import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"text/tabwriter"

"github.com/pkg/errors"
"github.com/urfave/cli"

"github.com/rootless-containers/rootlesskit/pkg/port"
"github.com/rootless-containers/rootlesskit/pkg/port/portutil"
)

var listPortsCommand = cli.Command{
Name: "list-ports",
Usage: "List ports",
ArgsUsage: "[flags]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "json",
Usage: "Prints as JSON",
},
},
Action: listPortsAction,
}

func listPortsAction(clicontext *cli.Context) error {
c, err := newClient(clicontext)
if err != nil {
return err
}
pm := c.PortManager()
ctx := context.Background()
portStatuses, err := pm.ListPorts(ctx)
if err != nil {
return err
}
if clicontext.Bool("json") {
// Marshal per entry, for consistency with add-ports
// (and for potential streaming support)
for _, p := range portStatuses {
m, err := json.Marshal(p)
if err != nil {
return err
}
fmt.Println(string(m))
}
return nil
}
w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
if _, err := fmt.Fprintln(w, "ID\tPROTO\tPARENTIP\tPARENTPORT\tCHILDPORT\t"); err != nil {
return err
}
for _, p := range portStatuses {
if _, err := fmt.Fprintf(w, "%d\t%s\t%s\t%d\t%d\t\n",
p.ID, p.Spec.Proto, p.Spec.ParentIP, p.Spec.ParentPort, p.Spec.ChildPort); err != nil {
return err
}
}
return w.Flush()
}

var addPortsCommand = cli.Command{
Name: "add-ports",
Usage: "Add ports",
ArgsUsage: "[flags] PARENTIP:PARENTPORT:CHILDPORT/PROTO [PARENTIP:PARENTPORT:CHILDPORT/PROTO...]",
Description: "Add exposed ports. The port spec is similar to `docker run -p`. e.g. \"127.0.0.1:8080:80/tcp\".",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "json",
Usage: "Prints as JSON",
},
},
Action: addPortsAction,
}

func addPortsAction(clicontext *cli.Context) error {
if clicontext.NArg() < 1 {
return errors.New("no port specified")
}
var portSpecs []port.Spec
for _, s := range clicontext.Args() {
sp, err := portutil.ParsePortSpec(s)
if err != nil {
return err
}
portSpecs = append(portSpecs, *sp)
}

c, err := newClient(clicontext)
if err != nil {
return err
}
pm := c.PortManager()
ctx := context.Background()
for _, sp := range portSpecs {
portStatus, err := pm.AddPort(ctx, sp)
if err != nil {
return err
}
if clicontext.Bool("json") {
m, err := json.Marshal(portStatus)
if err != nil {
return err
}
fmt.Println(string(m))
} else {
fmt.Printf("%d\n", portStatus.ID)
}
}
return nil
}

var removePortsCommand = cli.Command{
Name: "remove-ports",
Usage: "Remove ports",
ArgsUsage: "[flags] ID [ID...]",
Action: removePortsAction,
}

func removePortsAction(clicontext *cli.Context) error {
if clicontext.NArg() < 1 {
return errors.New("no ID specified")
}
var ids []int
for _, s := range clicontext.Args() {
id, err := strconv.Atoi(s)
if err != nil {
return err
}
ids = append(ids, id)
}
c, err := newClient(clicontext)
if err != nil {
return err
}
pm := c.PortManager()
ctx := context.Background()
for _, id := range ids {
if err := pm.RemovePort(ctx, id); err != nil {
return err
}
fmt.Printf("%d\n", id)
}
return nil
}

0 comments on commit e147262

Please sign in to comment.