Skip to content

Commit

Permalink
Port API: support specifying IP version explicitly ("tcp4", "tcp6")
Browse files Browse the repository at this point in the history
Fix rootless-containers#231

Signed-off-by: Akihiro Suda <[email protected]>
  • Loading branch information
AkihiroSuda authored and youmeim committed Dec 18, 2024
1 parent 61599dc commit 18d3f55
Show file tree
Hide file tree
Showing 15 changed files with 171 additions and 26 deletions.
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
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
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.
80 changes: 80 additions & 0 deletions hack/integration-port.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/bin/bash
source $(realpath $(dirname $0))/common.inc.sh

# test_port PORT_DRIVER CURL_URL EXPECTATION [ROOTLESSKIT ARGS...]
function test_port() {
args="$@"
port_driver="$1"
curl_url="$2"
expectation="$3"
shift
shift
shift
rootlesskit_args="$@"
INFO "Testing port_driver=\"${port_driver}\" curl_url=\"${curl_url}\" expectation=\"${expectation}\" rootlesskit_args=\"${rootlesskit_args}\""
tmp=$(mktemp -d)
state_dir=${tmp}/state
html_dir=${tmp}/html
mkdir -p ${html_dir}
echo "test_port ($args)" >${html_dir}/index.html
$ROOTLESSKIT \
--state-dir=${state_dir} \
--net=slirp4netns \
--disable-host-loopback \
--copy-up=/etc \
--port-driver=${port_driver} \
${rootlesskit_args} \
busybox httpd -f -v -p 80 -h ${html_dir} \
2>&1 &
pid=$!
sleep 1

set +e
curl -fsSL ${curl_url}
code=$?
set -e
if [ "${expectation}" = "should success" ]; then
if [ ${code} != 0 ]; then
ERROR "curl exited with ${code}"
exit ${code}
fi
elif [ "${expectation}" = "should fail" ]; then
if [ ${code} = 0 ]; then
ERROR "curl should not success"
exit 1
fi
else
ERROR "internal error"
exit 1
fi

INFO "Test pasing, stopping httpd (\"exit status 255\" is negligible here)"
kill -SIGTERM $(cat ${state_dir}/child_pid)
wait $pid >/dev/null 2>&1 || true
rm -rf $tmp
}

INFO "===== Port driver: builtin ====="
INFO "=== protocol \"tcp\" listens on both v4 and v6 ==="
test_port builtin http://127.0.0.1:8080 "should success" -p 0.0.0.0:8080:80/tcp
test_port builtin http://[::1]:8080 "should success" -p 0.0.0.0:8080:80/tcp

INFO "=== protocol \"tcp4\" is strictly v4-only ==="
test_port builtin http://127.0.0.1:8080 "should success" -p 0.0.0.0:8080:80/tcp4
test_port builtin http://[::1]:8080 "should fail" -p 0.0.0.0:8080:80/tcp4

INFO "=== protocol \"tcp6\" is strictly v4-only ==="
test_port builtin http://127.0.0.1:8080 "should fail" -p [::]:8080:80/tcp6
test_port builtin http://[::1]:8080 "should success" -p [::]:8080:80/tcp6

INFO "=== \"tcp4\" and \"tcp6\" do not conflict ==="
test_port builtin http://127.0.0.1:8080 "should success" -p 0.0.0.0:8080:80/tcp4 -p [::]:8080:80/tcp6

INFO "===== Port driver: slirp4netns (IPv4 only)====="
INFO "=== protocol \"tcp\" listens on v4 ==="
test_port slirp4netns http://127.0.0.1:8080 "should success" -p 0.0.0.0:8080:80/tcp

INFO "=== protocol \"tcp4\" is strictly v4-only ==="
test_port slirp4netns http://[::1]:8080 "should fail" -p 0.0.0.0:8080:80/tcp4

INFO "===== PASSING ====="
8 changes: 7 additions & 1 deletion pkg/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,17 @@ components:
schemas:
Proto:
type: string
description: "protocol for listening"
description: "protocol for listening. Corresponds to Go's net.Listen. The strings with \"4\" and \"6\" suffixes were introduced in API 1.1.0."
enum:
- tcp
- tcp4
- tcp6
- udp
- udp4
- udp6
- sctp
- sctp4
- sctp6
PortSpec:
required:
- proto
Expand Down
9 changes: 8 additions & 1 deletion pkg/port/builtin/child/child.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"net"
"os"
"strings"

"github.com/pkg/errors"
"golang.org/x/sys/unix"
Expand Down Expand Up @@ -101,10 +102,16 @@ func (d *childDriver) handleConnectInit(c *net.UnixConn, req *msg.Request) error
func (d *childDriver) handleConnectRequest(c *net.UnixConn, req *msg.Request) error {
switch req.Proto {
case "tcp":
case "tcp4":
case "tcp6":
case "udp":
case "udp4":
case "udp6":
default:
return errors.Errorf("unknown proto: %q", req.Proto)
}
// dialProto is always non-v6
dialProto := strings.TrimSuffix(req.Proto, "6")
var dialer net.Dialer
ip := req.IP
if ip == "" {
Expand All @@ -120,7 +127,7 @@ func (d *childDriver) handleConnectRequest(c *net.UnixConn, req *msg.Request) er
}
ip = p.String()
}
targetConn, err := dialer.Dial(req.Proto, fmt.Sprintf("%s:%d", ip, req.Port))
targetConn, err := dialer.Dial(dialProto, fmt.Sprintf("%s:%d", ip, req.Port))
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion pkg/port/builtin/msg/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const (
// Request and Response are encoded as JSON with uint32le length header.
type Request struct {
Type string // "init" or "connect"
Proto string // "tcp" or "udp"
Proto string // "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"
IP string
Port int
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/port/builtin/parent/parent.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type driver struct {
func (d *driver) Info(ctx context.Context) (*api.PortDriverInfo, error) {
info := &api.PortDriverInfo{
Driver: "builtin",
Protos: []string{"tcp", "udp"},
Protos: []string{"tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"},
}
return info, nil
}
Expand Down Expand Up @@ -143,9 +143,9 @@ func (d *driver) AddPort(ctx context.Context, spec port.Spec) (*port.Status, err
return nil // FIXME
}
switch spec.Proto {
case "tcp":
case "tcp", "tcp4", "tcp6":
err = tcp.Run(d.socketPath, spec, routineStopCh, d.logWriter)
case "udp":
case "udp", "udp4", "udp6":
err = udp.Run(d.socketPath, spec, routineStopCh, d.logWriter)
default:
// NOTREACHED
Expand Down
2 changes: 1 addition & 1 deletion pkg/port/builtin/parent/tcp/tcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, logWriter io.Writer) error {
ln, err := net.Listen("tcp", net.JoinHostPort(spec.ParentIP, strconv.Itoa(spec.ParentPort)))
ln, err := net.Listen(spec.Proto, net.JoinHostPort(spec.ParentIP, strconv.Itoa(spec.ParentPort)))
if err != nil {
fmt.Fprintf(logWriter, "listen: %v\n", err)
return err
Expand Down
4 changes: 2 additions & 2 deletions pkg/port/builtin/parent/udp/udp.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import (
)

func Run(socketPath string, spec port.Spec, stopCh <-chan struct{}, logWriter io.Writer) error {
addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(spec.ParentIP, strconv.Itoa(spec.ParentPort)))
addr, err := net.ResolveUDPAddr(spec.Proto, net.JoinHostPort(spec.ParentIP, strconv.Itoa(spec.ParentPort)))
if err != nil {
return err
}
c, err := net.ListenUDP("udp", addr)
c, err := net.ListenUDP(spec.Proto, addr)
if err != nil {
return err
}
Expand Down
6 changes: 4 additions & 2 deletions pkg/port/port.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import (
)

type Spec struct {
Proto string `json:"proto,omitempty"` // either "tcp" or "udp". in future "sctp" will be supported as well.
ParentIP string `json:"parentIP,omitempty"` // IPv4 address. can be empty (0.0.0.0).
// Proto is one of ["tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"].
// "tcp" may cause listening on both IPv4 and IPv6. (Corresponds to Go's net.Listen .)
Proto string `json:"proto,omitempty"`
ParentIP string `json:"parentIP,omitempty"` // IPv4 or IPv6 address. can be empty (0.0.0.0).
ParentPort int `json:"parentPort,omitempty"`
ChildPort int `json:"childPort,omitempty"`
// ChildIP is an IPv4 address.
Expand Down
5 changes: 4 additions & 1 deletion pkg/port/portutil/portutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@ func ValidatePortSpec(spec port.Spec, existingPorts map[int]*port.Status) error

func validateProto(proto string) error {
switch proto {
case "tcp", "udp", "sctp":
case
"tcp", "tcp4", "tcp6",
"udp", "udp4", "udp6",
"sctp", "sctp4", "sctp6":
return nil
default:
return errors.Errorf("unknown proto: %q", proto)
Expand Down
9 changes: 9 additions & 0 deletions pkg/port/portutil/portutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ func TestParsePortSpec(t *testing.T) {
ChildPort: 80,
},
},
{
s: "127.0.0.1:8080:80/tcp4",
expected: &port.Spec{
Proto: "tcp4",
ParentIP: "127.0.0.1",
ParentPort: 8080,
ChildPort: 80,
},
},
{
s: "127.0.0.1:8080:10.0.2.100:80/tcp",
expected: &port.Spec{
Expand Down
10 changes: 8 additions & 2 deletions pkg/port/slirp4netns/slirp4netns.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io"
"io/ioutil"
"net"
"strings"
"sync"

"github.com/pkg/errors"
Expand Down Expand Up @@ -38,7 +39,8 @@ type driver struct {
func (d *driver) Info(ctx context.Context) (*api.PortDriverInfo, error) {
info := &api.PortDriverInfo{
Driver: "slirp4netns",
Protos: []string{"tcp", "udp"},
// No IPv6 support yet
Protos: []string{"tcp", "tcp4", "udp", "udp4"},
}
return info, nil
}
Expand All @@ -64,6 +66,10 @@ func (d *driver) AddPort(ctx context.Context, spec port.Spec) (*port.Status, err
if err != nil {
return nil, err
}
if strings.HasSuffix(spec.Proto, "6") {
return nil, errors.Errorf("unsupported protocol %q", spec.Proto)
}
proto := strings.TrimSuffix(spec.Proto, "4")
ip := spec.ChildIP
if ip == "" {
ip = d.childIP
Expand All @@ -81,7 +87,7 @@ func (d *driver) AddPort(ctx context.Context, spec port.Spec) (*port.Status, err
req := request{
Execute: "add_hostfwd",
Arguments: addHostFwdArguments{
Proto: spec.Proto,
Proto: proto,
HostAddr: spec.ParentIP,
HostPort: spec.ParentPort,
GuestAddr: ip,
Expand Down
16 changes: 10 additions & 6 deletions pkg/port/socat/socat.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type driver struct {
func (d *driver) Info(ctx context.Context) (*api.PortDriverInfo, error) {
info := &api.PortDriverInfo{
Driver: "socat",
Protos: []string{"tcp", "udp"},
Protos: []string{"tcp", "tcp4", "udp", "udp4"},
}
return info, nil
}
Expand Down Expand Up @@ -77,6 +77,11 @@ func (d *driver) AddPort(ctx context.Context, spec port.Spec) (*port.Status, err
if err != nil {
return nil, err
}
switch spec.Proto {
case "tcp", "tcp4", "udp", "udp4":
default:
return nil, errors.Errorf("unsupported proto: %s", spec.Proto)
}
cf := func() (*exec.Cmd, error) {
return createSocatCmd(ctx, spec, d.logWriter, d.childPID)
}
Expand Down Expand Up @@ -124,9 +129,6 @@ func (d *driver) RemovePort(ctx context.Context, id int) error {
}

func createSocatCmd(ctx context.Context, spec port.Spec, logWriter io.Writer, childPID int) (*exec.Cmd, error) {
if spec.Proto != "tcp" && spec.Proto != "udp" {
return nil, errors.Errorf("unsupported proto: %s", spec.Proto)
}
ipStr := "0.0.0.0"
if spec.ParentIP != "" {
ip := net.ParseIP(spec.ParentIP)
Expand Down Expand Up @@ -165,10 +167,12 @@ func createSocatCmd(ctx context.Context, spec port.Spec, logWriter io.Writer, ch
hp = net.JoinHostPort(ip, strconv.Itoa(spec.ChildPort))
)
switch spec.Proto {
case "tcp":
case "tcp", "tcp4":
proto = "TCP"
case "udp":
case "udp", "udp4":
proto = "UDP"
default:
panic("should not reach here")
}
cmd = exec.CommandContext(ctx,
"socat",
Expand Down
Loading

0 comments on commit 18d3f55

Please sign in to comment.