Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Port API: support specifying IP version explicitly ("tcp4", "tcp6") #232

Merged
merged 4 commits into from
Mar 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
run: docker run --rm --privileged rootlesskit:test-integration sh -exc "sudo mount --make-rshared / && ./integration-propagation.sh"
- name: "Integration test: restart"
run: docker run --rm --privileged rootlesskit:test-integration ./integration-restart.sh
- name: "Integration test: port"
# NOTE: "--net=host" is a bad hack to enable IPv6
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

run: docker run --rm --net=host --privileged rootlesskit:test-integration ./integration-port.sh
# ===== Benchmark: Network (MTU=1500) =====
- name: "Benchmark: Network (MTU=1500, network driver=slirp4netns)"
run: |
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ OPTIONS:
The following files will be created in the state directory, which can be specified with `--state-dir`:
* `lock`: lock file
* `child_pid`: decimal PID text that can be used for `nsenter(1)`.
* `api.sock`: REST API socket for `rootlessctl`. See [Port Drivers](./docs/port.md) section.
* `api.sock`: REST API socket. See [`./docs/api.md`](./docs/api.md) and [`./docs/port.md`](./docs/port.md).

If `--state-dir` is not specified, RootlessKit creates a temporary state directory on `/tmp` and removes it on exit.

Expand All @@ -248,3 +248,4 @@ Undocumented environment variables are subject to change.
- [`./docs/port.md`](./docs/port.md): Port forwarding (`--port-driver`, `-p`, ...)
- [`./docs/mount.md`](./docs/mount.md): Mount (`--propagation`, ...)
- [`./docs/process.md`](./docs/process.md): Process (`--pidns`, `--reaper`, `--cgroupns`, `--evacuate-cgroup2`, ...)
- [`./docs/api.md`](./docs/api.md): REST API
56 changes: 56 additions & 0 deletions cmd/rootlessctl/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"context"
"encoding/json"
"fmt"

"github.com/urfave/cli/v2"
)

var infoCommand = cli.Command{
Name: "info",
Usage: "Show info",
ArgsUsage: "[flags]",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "json",
Usage: "Prints as JSON",
},
},
Action: infoAction,
}

func infoAction(clicontext *cli.Context) error {
w := clicontext.App.Writer
c, err := newClient(clicontext)
if err != nil {
return err
}
ctx := context.Background()
info, err := c.Info(ctx)
if err != nil {
return err
}
if clicontext.Bool("json") {
m, err := json.MarshalIndent(info, "", " ")
if err != nil {
return err
}
fmt.Fprintln(w, string(m))
return nil
}
fmt.Fprintf(w, "- REST API version: %s\n", info.APIVersion)
fmt.Fprintf(w, "- Implementation version: %s\n", info.Version)
fmt.Fprintf(w, "- State Directory: %s\n", info.StateDir)
fmt.Fprintf(w, "- Child PID: %d\n", info.ChildPID)
if info.NetworkDriver != nil {
fmt.Fprintf(w, "- Network Driver: %s\n", info.NetworkDriver.Driver)
fmt.Fprintf(w, " - DNS: %v\n", info.NetworkDriver.DNS)
}
if info.PortDriver != nil {
fmt.Fprintf(w, "- Port Driver: %s\n", info.PortDriver.Driver)
fmt.Fprintf(w, " - Supported protocols: %v\n", info.PortDriver.Protos)
}
return nil
}
1 change: 1 addition & 0 deletions cmd/rootlessctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func main() {
&listPortsCommand,
&addPortsCommand,
&removePortsCommand,
&infoCommand,
}
app.Before = func(clicontext *cli.Context) error {
if debug {
Expand Down
107 changes: 97 additions & 10 deletions cmd/rootlesskit-docker-proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import (
"flag"
"fmt"
"log"
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"

"github.com/rootless-containers/rootlesskit/pkg/api/client"
"github.com/rootless-containers/rootlesskit/pkg/port"
"github.com/sirupsen/logrus"

"github.com/pkg/errors"
)
Expand All @@ -35,6 +38,85 @@ func main() {
}
}

func isIPv6(ipStr string) bool {
ip := net.ParseIP(ipStr)
if ip == nil {
return false
}
return ip.To4() == nil
}

func getPortDriverProtos(c client.Client) (string, map[string]struct{}, error) {
info, err := c.Info(context.Background())
if err != nil {
return "", nil, errors.Wrap(err, "failed to call info API, probably RootlessKit binary is too old (needs to be v0.14.0 or later)")
}
if info.PortDriver == nil {
return "", nil, errors.New("no port driver is available")
}
m := make(map[string]struct{}, len(info.PortDriver.Protos))
for _, p := range info.PortDriver.Protos {
m[p] = struct{}{}
}
return info.PortDriver.Driver, m, nil
}

func callRootlessKitAPI(c client.Client,
hostIP string, hostPort int,
dockerProxyProto, childIP string) (func() error, error) {
// dockerProxyProto is like "tcp", but we need to convert it to "tcp4" or "tcp6" explicitly
// for libnetwork >= 20201216
//
// See https://github.com/moby/libnetwork/pull/2604/files#diff-8fa48beed55dd033bf8e4f8c40b31cf69d0b2cc5d4bb53cde8594670ea6c938aR20
// See also https://github.com/rootless-containers/rootlesskit/issues/231
apiProto := dockerProxyProto
if !strings.HasSuffix(apiProto, "4") && !strings.HasSuffix(apiProto, "6") {
if isIPv6(hostIP) {
apiProto += "6"
} else {
apiProto += "4"
}
}
portDriverName, apiProtos, err := getPortDriverProtos(c)
if err != nil {
return nil, err
}
if _, ok := apiProtos[apiProto]; !ok {
// This happens when apiProto="tcp6", portDriverName="slirp4netns",
// because "slirp4netns" port driver does not support listening on IPv6 yet.
//
// Note that "slirp4netns" port driver is not used by default,
// even when network driver is set to "slirp4netns".
//
// Most users are using "builtin" port driver and will not see this warning.
logrus.Warnf("protocol %q is not supported by the RootlessKit port driver %q, ignoring request for %q",
apiProto,
portDriverName,
net.JoinHostPort(hostIP, strconv.Itoa(hostPort)))
return nil, nil
}

pm := c.PortManager()
p := port.Spec{
Proto: apiProto,
ParentIP: hostIP,
ParentPort: hostPort,
ChildIP: childIP,
ChildPort: hostPort,
}
st, err := pm.AddPort(context.Background(), p)
if err != nil {
return nil, errors.Wrap(err, "error while calling PortManager.AddPort()")
}
deferFunc := func() error {
if dErr := pm.RemovePort(context.Background(), st.ID); dErr != nil {
return errors.Wrap(err, "error while calling PortManager.RemovePort()")
}
return nil
}
return deferFunc, nil
}

func xmain(f *os.File) error {
containerIP := flag.String("container-ip", "", "container ip")
containerPort := flag.Int("container-port", -1, "container port")
Expand All @@ -52,23 +134,28 @@ func xmain(f *os.File) error {
if err != nil {
return errors.Wrap(err, "error while connecting to RootlessKit API socket")
}
pm := c.PortManager()
p := port.Spec{
Proto: *proto,
ParentIP: *hostIP,
ParentPort: *hostPort,
ChildPort: *hostPort,

childIP := "127.0.0.1"
if isIPv6(*hostIP) {
childIP = "::1"
}

deferFunc, err := callRootlessKitAPI(c, *hostIP, *hostPort, *proto, childIP)
if deferFunc != nil {
defer func() {
if dErr := deferFunc(); dErr != nil {
logrus.Warn(dErr)
}
}()
}
st, err := pm.AddPort(context.Background(), p)
if err != nil {
return errors.Wrap(err, "error while calling PortManager.AddPort()")
return err
}
defer pm.RemovePort(context.Background(), st.ID)

cmd := exec.Command(realProxy,
"-container-ip", *containerIP,
"-container-port", strconv.Itoa(*containerPort),
"-host-ip", "127.0.0.1",
"-host-ip", childIP,
"-host-port", strconv.Itoa(*hostPort),
"-proto", *proto)
cmd.Stdout = os.Stdout
Expand Down
62 changes: 62 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# REST API

RootlessKit listens REST API on `${ROOTLESSKIT_STATE_DIR}/api.sock`.

```console
(host)$ rootlesskit --net=slirp4netns --port-driver=builtin bash
(rootlesskit)# curl -s --unix-socket "${ROOTLESSKIT_STATE_DIR}/api.sock" http://rootlesskit/v1/info | jq .
{
"apiVersion": "1.1.0",
"version": "0.13.2+dev",
"stateDir": "/tmp/rootlesskit957151185",
"childPID": 157684,
"networkDriver": {
"driver": "slirp4netns",
"dns": [
"10.0.2.3"
]
},
"portDriver": {
"driver": "builtin",
"protos": [
"tcp",
"udp"
]
}
}
```

## openapi.yaml

See [`../pkg/api/openapi.yaml`](../pkg/api/openapi.yaml)

## rootlessctl CLI

`rootlessctl` is the CLI for the API.

```console
$ rootlessctl --help
NAME:
rootlessctl - RootlessKit API client

USAGE:
rootlessctl [global options] command [command options] [arguments...]

VERSION:
0.13.2+dev

COMMANDS:
list-ports List ports
add-ports Add ports
remove-ports Remove ports
info Show info
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
--debug debug mode (default: false)
--socket value Path to api.sock (under the "rootlesskit --state-dir" directory), defaults to $ROOTLESSKIT_STATE_DIR/api.sock
--help, -h show help (default: false)
--version, -v print the version (default: false)
```

e.g., `rootlessctl --socket /foo/bar/sock info --json`
14 changes: 14 additions & 0 deletions docs/port.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,17 @@ If you are using `builtin` driver, you can expose the privileged ports without c
```console
$ sudo setcap cap_net_bind_service=ep $(pwd rootlesskit)
```

### Note about IPv6

Specifying `0.0.0.0:8080:80/tcp` may cause listening on IPv6 as well as on IPv4.
Same applies to `[::]:8080:80/tcp`.

This behavior may sound weird but corresponds to [Go's behavior](https://github.com/golang/go/commit/071908f3d809245eda42bf6eab071c323c67b7d2),
so this is not a bug.

To specify IPv4 explicitly, use `tcp4` instead of `tcp`, e.g., `0.0.0.0:8080:80/tcp4`.
To specify IPv6 explicitly, use `tcp6`, e.g., `[::]:8080:80/tcp6`.

The `tcp4` and `tcp6` forms were introduced in RootlessKit v0.14.0.
The `tcp6` is currently supported only for `builtin` port driver.
Loading