Skip to content
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
245 changes: 192 additions & 53 deletions api/next.txtpb

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ service SocketForward {
// Bind sets up the socket forward entries on the VM side. The VM creates
// UNIX listener sockets at the paths given in socket_path. This RPC returns
// only after all socket files have been created, so the caller can safely
// proceed with container creation (crun bind mounts).
// proceed with container creation (crun bind mounts). The shim and guest
// versions are coupled, including the private socket mode contract.
rpc Bind(BindRequest) returns (google.protobuf.Empty);

// Accept is a bidirectional streaming RPC used to coordinate forwarded
Expand Down Expand Up @@ -68,6 +69,11 @@ message Socket {
// the shim has already rewritten the uds mount to a bind mount from
// this path to the user-specified destination inside the container.
string socket_path = 2;
// Zero preserves the legacy socket mode and owner. Mode 0600 requests a
// private listener owned by UID and GID in the VM.
uint32 mode = 3;
uint32 uid = 4;
uint32 gid = 5;
}

// ConnectRequest is sent by the VM on the Accept stream to notify the host of
Expand Down
95 changes: 62 additions & 33 deletions api/services/socketforward/v1/socketforward.pb.go

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

56 changes: 45 additions & 11 deletions internal/shim/task/socketforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"io"
"net"
"strconv"
"strings"

"github.com/containerd/log"
Expand Down Expand Up @@ -55,6 +56,9 @@ type socketForwardEntry struct {
// containerPath is the user-specified destination: where the socket
// appears inside the container rootfs.
containerPath string
mode uint32
uid uint32
gid uint32
}

// socketForwardsProvider parses OCI mounts with type "uds" and generates the
Expand All @@ -72,7 +76,8 @@ type socketForwardsProvider struct {
// UDS mount fields:
// - Source: host-side UNIX socket path (the target service)
// - Destination: container-side UNIX socket path
// - Options: none
// - Options: none, or uid=<numeric UID>,gid=<numeric GID> to create a
// private (0600) guest listener owned by that user and group
//
// Each UDS mount is replaced in-place with a bind mount whose source is
// vmPath (/run/socketfwd/{id}.sock) and whose destination is the original
Expand Down Expand Up @@ -115,8 +120,27 @@ func parseUDSMount(containerID string, m specs.Mount) (socketForwardEntry, error
if m.Destination == "" {
return socketForwardEntry{}, fmt.Errorf("destination (container path) is required")
}
if len(m.Options) > 0 {
return socketForwardEntry{}, fmt.Errorf("unknown option %q", strings.Join(m.Options, ", "))
var mode uint32
var uid, gid uint32
if len(m.Options) != 0 {
if len(m.Options) != 2 || !strings.HasPrefix(m.Options[0], "uid=") || !strings.HasPrefix(m.Options[1], "gid=") {
return socketForwardEntry{}, fmt.Errorf("unknown option %q", strings.Join(m.Options, ", "))
}
parsedUID, err := strconv.ParseUint(strings.TrimPrefix(m.Options[0], "uid="), 10, 32)
if err != nil {
return socketForwardEntry{}, fmt.Errorf("invalid guest socket UID: %w", err)
}
if parsedUID == uint64(^uint32(0)) {
return socketForwardEntry{}, fmt.Errorf("invalid guest socket UID: reserved chown no-change value")
}
parsedGID, err := strconv.ParseUint(strings.TrimPrefix(m.Options[1], "gid="), 10, 32)
if err != nil {
return socketForwardEntry{}, fmt.Errorf("invalid guest socket GID: %w", err)
}
if parsedGID == uint64(^uint32(0)) {
return socketForwardEntry{}, fmt.Errorf("invalid guest socket GID: reserved chown no-change value")
}
mode, uid, gid = 0600, uint32(parsedUID), uint32(parsedGID)
}

hash := sha256.Sum256([]byte(containerID + ":" + m.Destination))
Expand All @@ -125,6 +149,9 @@ func parseUDSMount(containerID string, m specs.Mount) (socketForwardEntry, error
hostPath: m.Source,
vmPath: fmt.Sprintf("/run/socketfwd/%x.sock", hash),
containerPath: m.Destination,
mode: mode,
uid: uid,
gid: gid,
}, nil
}

Expand All @@ -146,13 +173,12 @@ func bindSockets(ctx context.Context, sb sandbox.Sandbox, entries []socketForwar
sockets = append(sockets, &socketforward.Socket{
ForwardID: e.id,
SocketPath: e.vmPath,
Mode: e.mode,
Uid: e.uid,
Gid: e.gid,
})
}

sfClient := socketforward.NewTTRPCSocketForwardClient(vmc)
if _, err := sfClient.Bind(ctx, &socketforward.BindRequest{
Sockets: sockets,
}); err != nil {
if _, err := socketforward.NewTTRPCSocketForwardClient(vmc).Bind(ctx, &socketforward.BindRequest{Sockets: sockets}); err != nil {
Comment thread
austinvazquez marked this conversation as resolved.
return fmt.Errorf("bind RPC: %w", err)
}
return nil
Expand Down Expand Up @@ -291,22 +317,30 @@ func (fwd *socketForwarder) handleConnection(ctx context.Context, sb sandbox.San
// closes or errors. Both connections are closed when done.
func relay(a, b io.ReadWriteCloser, closeCh <-chan struct{}) {
done := make(chan struct{}, 2)
relayDone := make(chan struct{})
cp := func(dst io.Writer, src io.Reader) {
io.Copy(dst, src)
done <- struct{}{}
}
go cp(a, b)
go cp(b, a)
watcherDone := make(chan struct{})
go func() {
<-closeCh
a.Close()
b.Close()
defer close(watcherDone)
select {
case <-closeCh:
a.Close()
b.Close()
case <-relayDone:
}
}()

<-done // one side finished
a.Close()
b.Close()
<-done // wait for the other
close(relayDone)
<-watcherDone
}

func (fwd *socketForwarder) shutdown() error {
Expand Down
106 changes: 106 additions & 0 deletions internal/shim/task/socketforward_bind_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package task

import (
"context"
"errors"
"net"
"testing"

"github.com/containerd/ttrpc"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/emptypb"

socketforward "github.com/containerd/nerdbox/api/services/socketforward/v1"
"github.com/containerd/nerdbox/internal/shim/sandbox"
)

type socketForwardTestSandbox struct {
sandbox.Sandbox
client *ttrpc.Client
}

func (s socketForwardTestSandbox) Client() (*ttrpc.Client, error) { return s.client, nil }

func TestBindSocketsPassesRequestedMode(t *testing.T) {
for _, tc := range []struct {
name string
mode uint32
}{
{name: "legacy"},
{name: "private", mode: 0600},
} {
t.Run(tc.name, func(t *testing.T) {
server, err := ttrpc.NewServer()
require.NoError(t, err)
bindRequests := make(chan *socketforward.BindRequest, 1)
server.Register("nerdbox.services.socketforward.v1.SocketForward", map[string]ttrpc.Method{
"Bind": func(_ context.Context, unmarshal func(interface{}) error) (interface{}, error) {
var req socketforward.BindRequest
if err := unmarshal(&req); err != nil {
return nil, err
}
bindRequests <- &req
return &emptypb.Empty{}, nil
},
})
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
go func() { _ = server.Serve(t.Context(), listener) }()
conn, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err)
client := ttrpc.NewClient(conn)
t.Cleanup(func() {
require.NoError(t, client.Close())
require.NoError(t, server.Close())
})

err = bindSockets(t.Context(), socketForwardTestSandbox{client: client}, []socketForwardEntry{{
id: "test", vmPath: "/run/socketfwd/test.sock", mode: tc.mode,
uid: 1000, gid: 1000,
}})
require.NoError(t, err)
req := <-bindRequests
require.Len(t, req.Sockets, 1)
require.Equal(t, tc.mode, req.Sockets[0].Mode)
require.Equal(t, uint32(1000), req.Sockets[0].Uid)
require.Equal(t, uint32(1000), req.Sockets[0].Gid)
})
}
}

func TestBindSocketsRejectsGuestFailure(t *testing.T) {
server, err := ttrpc.NewServer()
require.NoError(t, err)
server.Register("nerdbox.services.socketforward.v1.SocketForward", map[string]ttrpc.Method{
"Bind": func(context.Context, func(interface{}) error) (interface{}, error) {
return nil, errors.New("guest rejected mode")
},
})
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
go func() { _ = server.Serve(t.Context(), listener) }()
conn, err := net.Dial("tcp", listener.Addr().String())
require.NoError(t, err)
client := ttrpc.NewClient(conn)
t.Cleanup(func() {
require.NoError(t, client.Close())
require.NoError(t, server.Close())
})
require.ErrorContains(t, bindSockets(t.Context(), socketForwardTestSandbox{client: client}, []socketForwardEntry{{mode: 0600}}), "guest rejected mode")
}
Loading
Loading