From 7aa1c11b2a3e849c24014404335a39627dedf563 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 8 Jul 2026 21:57:50 -0700 Subject: [PATCH 1/5] Add shimtest networking suite Signed-off-by: Derek McGowan --- go.mod | 2 +- go.sum | 4 +- test/shim/shim_test.go | 7 + .../github.com/containerd/shimtest/README.md | 9 +- .../github.com/containerd/shimtest/helpers.go | 11 + .../containerd/shimtest/network_suite.go | 397 ++++++++++++++++++ .../github.com/containerd/shimtest/rootfs.go | 2 +- .../containerd/shimtest/testbin/testbin.go | 126 +++++- vendor/modules.txt | 2 +- 9 files changed, 543 insertions(+), 17 deletions(-) create mode 100644 vendor/github.com/containerd/shimtest/network_suite.go diff --git a/go.mod b/go.mod index a73085b8..0ef2ed5d 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/containerd/log v0.1.1-0.20260403072107-cb1839ebf76b github.com/containerd/otelttrpc v0.1.0 github.com/containerd/plugin v1.1.0 - github.com/containerd/shimtest v0.2.1 + github.com/containerd/shimtest v0.3.0 github.com/containerd/ttrpc v1.2.9-0.20260501231634-6c2eed2b612e github.com/containerd/typeurl/v2 v2.3.0 github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c diff --git a/go.sum b/go.sum index f2475760..97cf33f8 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,8 @@ github.com/containerd/platforms v1.0.0-rc.4 h1:M42JrUT4zfZTqtkUwkr0GzmUWbfyO5VO0 github.com/containerd/platforms v1.0.0-rc.4/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc= -github.com/containerd/shimtest v0.2.1 h1:v6DRcuU5TjwX9Qu+Q8suYvRp13UoJnDk5SPJKVIql3Q= -github.com/containerd/shimtest v0.2.1/go.mod h1:v9b7phlmKrfn9zKHqhDyoe0kv24mxDEYyJlXFcFhjnI= +github.com/containerd/shimtest v0.3.0 h1:oTtRnAEA20cqqxU68Jpg6fajUVXtUYI38Q1Z/shv70k= +github.com/containerd/shimtest v0.3.0/go.mod h1:v9b7phlmKrfn9zKHqhDyoe0kv24mxDEYyJlXFcFhjnI= github.com/containerd/ttrpc v1.2.9-0.20260501231634-6c2eed2b612e h1:uMP9FpdM40x+cvSyg6PiiINN9/b2908f8CsF/fZ438g= github.com/containerd/ttrpc v1.2.9-0.20260501231634-6c2eed2b612e/go.mod h1:IvZPGIALrdh9ZNv7AvRrKHCfwVPPCueLO2yuwj3KIqE= github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= diff --git a/test/shim/shim_test.go b/test/shim/shim_test.go index d9e8ab26..4c726156 100644 --- a/test/shim/shim_test.go +++ b/test/shim/shim_test.go @@ -83,6 +83,12 @@ func TestMain(m *testing.M) { // LayersSuite (HundredLayers) packs 101 erofs layers into a single // GPT-partitioned VMDK, consuming only one virtio-blk device regardless // of layer count. +// +// NetworkSuite verifies a container's default outbound network +// connectivity (TCP, UDP, DNS), independent of the mechanism a shim uses +// to provide it. This is the regression guard for TSI (Transparent Socket +// Impersonation), the default connectivity path for containers started +// without any network configuration. func TestShim(t *testing.T) { cfg := shimConfig() shimtest.NewRunSuite(cfg).Run(t) @@ -91,6 +97,7 @@ func TestShim(t *testing.T) { shimtest.NewOOMSuite(cfg).Run(t) shimtest.NewUDSSuite(cfg).Run(t) shimtest.NewLayersSuite(cfg).Run(t) + shimtest.NewNetworkSuite(cfg).Run(t) } // FuzzTransferMissing exercises the transfer service with arbitrary paths diff --git a/vendor/github.com/containerd/shimtest/README.md b/vendor/github.com/containerd/shimtest/README.md index c9084a6b..7f702f41 100644 --- a/vendor/github.com/containerd/shimtest/README.md +++ b/vendor/github.com/containerd/shimtest/README.md @@ -40,7 +40,7 @@ Tests are driven by one or more JSON configuration files. See | `uid` | int | UID to run as; defaults to the current user's UID. If set to a value different from the current UID and the effective UID is 0, the harness re-execs itself as that user via `sudo` | | `gid` | int | GID to run as | | `format_mounts` | bool | Provide the rootfs as formatted erofs/ext4 images with a `format/mkdir/overlay` descriptor for the shim to mount. Default (`false`) extracts the rootfs and provides a pre-mounted overlay (or plain directory when rootless) | -| `skip` | []string | Feature names to skip (`exec`, `layers`, `oom`, `transfer`, `uds`) | +| `skip` | []string | Feature names to skip (`exec`, `layers`, `net`, `oom`, `transfer`, `uds`) | | `env` | map | Additional environment variables for the test run | | `debug` | bool | Enable debug logging on the shim | @@ -110,6 +110,9 @@ config, the tree is `TestShim//`. | `TransferCopyToAndFrom` | transfer | Copy a file in and back out | | `TransferExecVerify` | transfer | Copy a file in, verify via exec | | `UDSRoundTrip` | uds | UDS socket forwarding round-trip | +| `OutboundTCP` | net | A container's init process opens an outbound TCP connection to a host-reachable endpoint and completes a round trip. Implementation-neutral: does not assume any particular networking mechanism, only that a container has working default outbound TCP connectivity, as "host networking" would provide. | +| `OutboundUDP` | net | A container's init process exchanges a UDP datagram with a host-reachable endpoint. Same neutrality as `OutboundTCP`, for the datagram path. | +| `DNSResolve` | net | A container's init process resolves a real external hostname (`example.com`, forcing an actual DNS query — not answered from `/etc/hosts`) and gets back valid IP addresses. Requires outbound internet access from the test host. | | `Stress` | (per feature) | Long-running concurrent stress run. Composes subtests from the enabled features (currently transfer: stat/write/read). Each subtest runs as a goroutine until the test deadline approaches or any one fails (which cancels the rest). Skipped under `-test.short`. | A separate top-level fuzz target exists alongside `TestShim`: @@ -230,8 +233,8 @@ unbounded `Stress` run, and run active fuzzing as its own step: - **`uid`**: omit to run as the runner user. Set explicitly when you want the harness to `sudo` re-exec itself or rewrite the profile. - **`skip`**: list of feature names to disable. Currently meaningful - values are `exec`, `layers`, `oom`, `transfer`, and `uds` — useful - when your shim doesn't implement transfer/UDS forwarding, + values are `exec`, `layers`, `net`, `oom`, `transfer`, and `uds` — + useful when your shim doesn't implement transfer/UDS forwarding, multi-layer rootfs descriptors, or when running rootless without cgroup delegation. diff --git a/vendor/github.com/containerd/shimtest/helpers.go b/vendor/github.com/containerd/shimtest/helpers.go index 86dadf75..3f641a9a 100644 --- a/vendor/github.com/containerd/shimtest/helpers.go +++ b/vendor/github.com/containerd/shimtest/helpers.go @@ -304,6 +304,17 @@ func newCreateTaskRequest(tb testing.TB, id, bundle, stdout, stderr string, root return req } +// newCreateTaskRequestStdin is like newCreateTaskRequest but also sets a stdin +// FIFO path on the request. Use this for tests where the init process reads +// from stdin (e.g. nc in TCP or UDP mode where the token is written by the +// test over a FIFO). +func newCreateTaskRequestStdin(tb testing.TB, id, bundle, stdin, stdout, stderr string, rootfs []*types.Mount) *taskAPI.CreateTaskRequest { + tb.Helper() + req := newCreateTaskRequest(tb, id, bundle, stdout, stderr, rootfs) + req.Stdin = stdin + return req +} + // bootstrapParams is the JSON / protobuf payload returned on stdout // from `shim start`. type bootstrapParams struct { diff --git a/vendor/github.com/containerd/shimtest/network_suite.go b/vendor/github.com/containerd/shimtest/network_suite.go new file mode 100644 index 00000000..e320f020 --- /dev/null +++ b/vendor/github.com/containerd/shimtest/network_suite.go @@ -0,0 +1,397 @@ +/* + 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 shimtest + +import ( + "bytes" + "fmt" + "net" + "strings" + "sync" + "testing" + "time" + + taskAPI "github.com/containerd/containerd/api/runtime/task/v3" + "github.com/containerd/containerd/v2/pkg/namespaces" + "github.com/containerd/ttrpc" +) + +// NetworkSuite contains conformance tests for a container's default +// network connectivity, gated on the "net" feature. +// +// These tests are deliberately agnostic to how a shim provides outbound +// connectivity (a host network namespace, a virtual NIC, a userspace proxy, +// or any other mechanism). They verify only the externally observable +// contract: a container started without any network configuration must +// behave like a process with working outbound networking — able to open +// TCP connections, exchange UDP datagrams, and resolve DNS names — exactly +// as "host networking" (no network namespace / no isolation) would provide. +// The mechanism a shim uses internally to satisfy this contract is not +// part of the API surface these tests check. +type NetworkSuite struct { + cfg Config +} + +// NewNetworkSuite constructs a NetworkSuite from the given options. +func NewNetworkSuite(cfg Config) *NetworkSuite { + return &NetworkSuite{cfg: cfg} +} + +// Run runs every test in the suite as a subtest of t. +func (s *NetworkSuite) Run(t *testing.T) { + t.Helper() + registerShimLeakCheck(t, s.cfg.ShimBinary) + t.Run("OutboundTCP", s.testOutboundTCP) + t.Run("OutboundUDP", s.testOutboundUDP) + t.Run("DNSResolve", s.testDNSResolve) +} + +// testOutboundTCP verifies that a container's init process can open an +// outbound TCP connection to a host-reachable endpoint and complete a +// round trip. +// +// The API contract: a container started without any network configuration +// must have a working default network path for outbound TCP, the same way +// a process using the host's network stack would. This test does not care +// how connectivity is achieved. +// +// The container runs nc(1) in TCP mode (nc ), which copies +// stdin to the connection and the connection to stdout. The test writes a +// token to the container's stdin FIFO; the host echoes it back; the test +// asserts that the container's stdout contains the echoed token. +func (s *NetworkSuite) testOutboundTCP(t *testing.T) { + shimBin, bundleDir, rootfsMounts := shimSetup(t, s.cfg) + cid := containerID(t) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("host tcp listen: %v", err) + } + t.Cleanup(func() { ln.Close() }) + + host, port, err := net.SplitHostPort(ln.Addr().String()) + if err != nil { + t.Fatalf("split addr: %v", err) + } + + const token = "network-suite-tcp-ok" + acceptDone := make(chan error, 1) + go func() { + conn, err := ln.Accept() + if err != nil { + acceptDone <- err + return + } + defer conn.Close() + conn.SetDeadline(time.Now().Add(10 * time.Second)) + buf := make([]byte, 256) + n, err := conn.Read(buf) + if err != nil && n == 0 { + acceptDone <- err + return + } + got := strings.TrimSpace(string(buf[:n])) + if got != token { + acceptDone <- fmt.Errorf("host received %q, want %q", got, token) + return + } + if _, err := fmt.Fprintf(conn, "%s\n", token); err != nil { + acceptDone <- err + return + } + // Close so nc's io.Copy from the socket returns and the container exits. + conn.Close() + acceptDone <- nil + }() + + // nc : TCP mode, bidirectional pipe between socket and stdio. + createOCISpec(t, bundleDir, []string{"/bin/nc", host, port}, s.cfg) + + stdinPath, stdoutPath, stderrPath := createStdioFifos(t, bundleDir) + ns := uniqueTestNamespace(t, "net") + ctx := namespaces.WithNamespace(t.Context(), ns) + + params := startShim(t, shimBin, bundleDir, cid, ns, s.cfg) + shimConn := connectShim(t, params.Address) + client := ttrpc.NewClient(shimConn) + defer client.Close() + + tc := taskAPI.NewTTRPCTaskClient(client) + + var stdoutBuf bytes.Buffer + var stdoutMu sync.Mutex + stdoutDone := drainFifoIntoDone(t, ctx, stdoutPath, &stdoutBuf, &stdoutMu) + drainFifo(t, ctx, stderrPath) + + if _, err := tc.Create(ctx, newCreateTaskRequestStdin(t, cid, bundleDir, stdinPath, stdoutPath, stderrPath, rootfsMounts)); err != nil { + t.Fatal("create failed:", err) + } + if _, err := tc.Start(ctx, &taskAPI.StartRequest{ID: cid}); err != nil { + t.Fatal("start failed:", err) + } + + // Write the token to the container's stdin so nc sends it to the host. + stdinFifo, err := openPipeWriter(ctx, stdinPath) + if err != nil { + t.Fatalf("open stdin fifo: %v", err) + } + if _, err := fmt.Fprintf(stdinFifo, "%s\n", token); err != nil { + t.Fatalf("write token to stdin: %v", err) + } + stdinFifo.Close() + + waitResp, err := tc.Wait(ctx, &taskAPI.WaitRequest{ID: cid}) + if err != nil { + t.Fatal("wait failed:", err) + } + <-stdoutDone + stdoutMu.Lock() + out := stdoutBuf.String() + stdoutMu.Unlock() + + if waitResp.ExitStatus != 0 { + t.Fatalf("container exit status: got %d, want 0; stdout: %q", waitResp.ExitStatus, out) + } + if !strings.Contains(out, token) { + t.Errorf("container stdout: want %q in output, got %q", token, out) + } + + select { + case err := <-acceptDone: + if err != nil { + t.Fatalf("host accept/echo: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("host did not observe a connection from the container within 5s") + } + + tc.Delete(ctx, &taskAPI.DeleteRequest{ID: cid}) + shutdownTask(ctx, tc, cid) + t.Log("container outbound TCP round-trip: ok") +} + +// testOutboundUDP verifies that a container's init process can exchange a +// UDP datagram with a host-reachable endpoint. +// +// The API contract: a container started without any network configuration +// must support outbound UDP datagrams, the same way a process using the +// host's network stack would. +// +// The container runs nc(1) in UDP mode (nc -u ), which sends +// stdin as a single datagram and writes the reply datagram to stdout. The +// test writes a token to the container's stdin FIFO; the host echoes it back; +// the test asserts that the container's stdout contains the echoed token. +func (s *NetworkSuite) testOutboundUDP(t *testing.T) { + shimBin, bundleDir, rootfsMounts := shimSetup(t, s.cfg) + cid := containerID(t) + + pc, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + t.Fatalf("host udp listen: %v", err) + } + t.Cleanup(func() { pc.Close() }) + + host, port, err := net.SplitHostPort(pc.LocalAddr().String()) + if err != nil { + t.Fatalf("split addr: %v", err) + } + + const token = "network-suite-udp-ok" + recvDone := make(chan error, 1) + go func() { + buf := make([]byte, 256) + pc.SetDeadline(time.Now().Add(10 * time.Second)) + n, addr, err := pc.ReadFrom(buf) + if err != nil { + recvDone <- err + return + } + got := strings.TrimSpace(string(buf[:n])) + if got != token { + recvDone <- fmt.Errorf("host received %q, want %q", got, token) + return + } + if _, err := pc.WriteTo([]byte(token), addr); err != nil { + recvDone <- err + return + } + recvDone <- nil + }() + + // nc -u : UDP mode, one sendto (stdin) then one recvfrom (stdout). + createOCISpec(t, bundleDir, []string{"/bin/nc", "-u", host, port}, s.cfg) + + stdinPath, stdoutPath, stderrPath := createStdioFifos(t, bundleDir) + ns := uniqueTestNamespace(t, "net") + ctx := namespaces.WithNamespace(t.Context(), ns) + + params := startShim(t, shimBin, bundleDir, cid, ns, s.cfg) + shimConn := connectShim(t, params.Address) + client := ttrpc.NewClient(shimConn) + defer client.Close() + + tc := taskAPI.NewTTRPCTaskClient(client) + + var stdoutBuf bytes.Buffer + var stdoutMu sync.Mutex + stdoutDone := drainFifoIntoDone(t, ctx, stdoutPath, &stdoutBuf, &stdoutMu) + drainFifo(t, ctx, stderrPath) + + if _, err := tc.Create(ctx, newCreateTaskRequestStdin(t, cid, bundleDir, stdinPath, stdoutPath, stderrPath, rootfsMounts)); err != nil { + t.Fatal("create failed:", err) + } + if _, err := tc.Start(ctx, &taskAPI.StartRequest{ID: cid}); err != nil { + t.Fatal("start failed:", err) + } + + // Write the token to stdin and close so nc reads EOF and sends one datagram. + stdinFifo, err := openPipeWriter(ctx, stdinPath) + if err != nil { + t.Fatalf("open stdin fifo: %v", err) + } + if _, err := fmt.Fprint(stdinFifo, token); err != nil { + t.Fatalf("write token to stdin: %v", err) + } + stdinFifo.Close() + + waitResp, err := tc.Wait(ctx, &taskAPI.WaitRequest{ID: cid}) + if err != nil { + t.Fatal("wait failed:", err) + } + <-stdoutDone + stdoutMu.Lock() + out := stdoutBuf.String() + stdoutMu.Unlock() + + if waitResp.ExitStatus != 0 { + t.Fatalf("container exit status: got %d, want 0; stdout: %q", waitResp.ExitStatus, out) + } + if !strings.Contains(out, token) { + t.Errorf("container stdout: want %q in output, got %q", token, out) + } + + select { + case err := <-recvDone: + if err != nil { + t.Fatalf("host receive/echo: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("host did not observe a datagram from the container within 5s") + } + + tc.Delete(ctx, &taskAPI.DeleteRequest{ID: cid}) + shutdownTask(ctx, tc, cid) + t.Log("container outbound UDP round-trip: ok") +} + +// dnsTestHostname is the hostname resolved by testDNSResolve. Resolving +// "localhost" would not exercise the network path at all — it is normally +// answered from /etc/hosts or the resolver's static handling without any +// DNS query. example.com is IANA-reserved for documentation and testing, +// extremely stable, and forces a real DNS query over the container's +// default network path. This is the one test in this suite that requires +// outbound internet access from the test host. +const dnsTestHostname = "example.com" + +// testDNSResolve verifies that a container's init process can resolve a +// hostname using the same DNS configuration the host would use. +// +// The API contract: a container started without any network configuration +// must be able to resolve DNS names via a resolver reachable through its +// default network path (typically the host's own resolver configuration, +// copied into the container when no explicit network is configured). +// +// The container runs host(1) (host ), which prints one line per address +// in the form " has address ". The test parses those lines and +// validates that each address field is a valid IP address. +func (s *NetworkSuite) testDNSResolve(t *testing.T) { + shimBin, bundleDir, rootfsMounts := shimSetup(t, s.cfg) + cid := containerID(t) + + // host : prints " has address " for each resolved address. + createOCISpec(t, bundleDir, []string{"/bin/host", dnsTestHostname}, s.cfg) + + stdoutPath, stderrPath := createIOFifos(t, bundleDir) + ns := uniqueTestNamespace(t, "net") + ctx := namespaces.WithNamespace(t.Context(), ns) + + params := startShim(t, shimBin, bundleDir, cid, ns, s.cfg) + shimConn := connectShim(t, params.Address) + client := ttrpc.NewClient(shimConn) + defer client.Close() + + tc := taskAPI.NewTTRPCTaskClient(client) + + var stdoutBuf bytes.Buffer + var stdoutMu sync.Mutex + stdoutDone := drainFifoIntoDone(t, ctx, stdoutPath, &stdoutBuf, &stdoutMu) + var stderrBuf bytes.Buffer + var stderrMu sync.Mutex + drainFifoInto(t, ctx, stderrPath, &stderrBuf, &stderrMu) + + if _, err := tc.Create(ctx, newCreateTaskRequest(t, cid, bundleDir, stdoutPath, stderrPath, rootfsMounts)); err != nil { + t.Fatal("create failed:", err) + } + if _, err := tc.Start(ctx, &taskAPI.StartRequest{ID: cid}); err != nil { + t.Fatal("start failed:", err) + } + + waitResp, err := tc.Wait(ctx, &taskAPI.WaitRequest{ID: cid}) + if err != nil { + t.Fatal("wait failed:", err) + } + <-stdoutDone + stdoutMu.Lock() + out := stdoutBuf.String() + stdoutMu.Unlock() + stderrMu.Lock() + errOut := stderrBuf.String() + stderrMu.Unlock() + + if waitResp.ExitStatus != 0 { + t.Fatalf("container exit status: got %d, want 0; stdout: %q, stderr: %q", waitResp.ExitStatus, out, errOut) + } + + // Parse "example.com has address 93.184.216.34" lines and validate each IP. + var addrs []string + for _, line := range strings.Split(strings.TrimSpace(out), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + // Expected format: " has address " + const marker = " has address " + idx := strings.Index(line, marker) + if idx < 0 { + t.Errorf("host %s: unexpected output line %q", dnsTestHostname, line) + continue + } + ip := line[idx+len(marker):] + if net.ParseIP(ip) == nil { + t.Errorf("host %s: %q is not a valid IP address", dnsTestHostname, ip) + continue + } + addrs = append(addrs, ip) + } + if len(addrs) == 0 { + t.Fatalf("host %s produced no addresses; stderr: %q", dnsTestHostname, errOut) + } + + tc.Delete(ctx, &taskAPI.DeleteRequest{ID: cid}) + shutdownTask(ctx, tc, cid) + t.Log("container DNS resolution: ok, addresses:", addrs) +} diff --git a/vendor/github.com/containerd/shimtest/rootfs.go b/vendor/github.com/containerd/shimtest/rootfs.go index f22eacb1..06244e92 100644 --- a/vendor/github.com/containerd/shimtest/rootfs.go +++ b/vendor/github.com/containerd/shimtest/rootfs.go @@ -125,7 +125,7 @@ func testbinAssetName(goarch string) string { // testbinCommands lists the commands provided by the testbin binary. // Symlinks are created in /bin for each command in the embedded // rootfs. -var testbinCommands = []string{"forever", "burstexit", "cat", "date", "echo", "exit", "hashverify", "layercheck", "ls", "memhog", "nc", "tickexit"} +var testbinCommands = []string{"forever", "burstexit", "cat", "date", "echo", "exit", "hashverify", "host", "layercheck", "ls", "memhog", "nc", "tickexit"} // bigFileSize is the size of the IO benchmark fixture file. Large // enough to swamp small per-call overheads while still building / diff --git a/vendor/github.com/containerd/shimtest/testbin/testbin.go b/vendor/github.com/containerd/shimtest/testbin/testbin.go index f1b93e28..fcc1baee 100644 --- a/vendor/github.com/containerd/shimtest/testbin/testbin.go +++ b/vendor/github.com/containerd/shimtest/testbin/testbin.go @@ -84,6 +84,8 @@ func Main() { cmdMemhog(args) case "nc": cmdNC(args) + case "host": + cmdHost(args) case "tickexit": cmdTickexit(args) default: @@ -444,20 +446,98 @@ func cmdBurstexit(args []string) { os.Exit(exitCode) } -// cmdNC connects to a unix domain socket and copies bidirectionally -// between the socket and stdio. Usage: nc -U +// cmdNC is a minimal netcat-compatible tool supporting three modes: +// +// nc -U connect to a unix domain socket +// nc connect via TCP +// nc -u exchange a single UDP datagram +// +// In all modes data flows verbatim between the network endpoint and stdio, +// matching the behaviour of the standard nc(1) utility: +// - TCP / unix: bidirectional io.Copy (stdin→socket, socket→stdout). +// - UDP: one unconnected sendto (stdin→remote) then one recvfrom +// (remote→stdout). The socket is unconnected (ListenPacket / WriteTo / +// ReadFrom, i.e. sendto/recvfrom) so that shim networking layers cannot +// short-circuit routing based on the local connect(2) call, which for UDP +// always succeeds regardless of whether any peer is listening. func cmdNC(args []string) { - if len(args) < 3 || args[1] != "-U" { - fmt.Fprintln(os.Stderr, "usage: nc -U ") + if len(args) < 2 { + fmt.Fprintln(os.Stderr, "usage: nc [-u] | nc -U ") os.Exit(1) } - conn, err := net.Dial("unix", args[2]) - if err != nil { - fmt.Fprintf(os.Stderr, "nc: %s: %v\n", args[2], err) - os.Exit(1) + + switch args[1] { + case "-U": + // Unix domain socket mode. + if len(args) < 3 { + fmt.Fprintln(os.Stderr, "usage: nc -U ") + os.Exit(1) + } + conn, err := net.Dial("unix", args[2]) + if err != nil { + fmt.Fprintf(os.Stderr, "nc: %s: %v\n", args[2], err) + os.Exit(1) + } + defer conn.Close() + ncStream(conn) + + case "-u": + // UDP datagram mode. + if len(args) < 4 { + fmt.Fprintln(os.Stderr, "usage: nc -u ") + os.Exit(1) + } + raddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(args[2], args[3])) + if err != nil { + fmt.Fprintf(os.Stderr, "nc: resolve %s:%s: %v\n", args[2], args[3], err) + os.Exit(1) + } + pc, err := net.ListenPacket("udp", ":0") + if err != nil { + fmt.Fprintf(os.Stderr, "nc: udp listen: %v\n", err) + os.Exit(1) + } + defer pc.Close() + + payload, err := io.ReadAll(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "nc: read stdin: %v\n", err) + os.Exit(1) + } + if _, err := pc.WriteTo(payload, raddr); err != nil { + fmt.Fprintf(os.Stderr, "nc: udp send: %v\n", err) + os.Exit(1) + } + buf := make([]byte, 65536) + n, _, err := pc.ReadFrom(buf) + if err != nil { + fmt.Fprintf(os.Stderr, "nc: udp recv: %v\n", err) + os.Exit(1) + } + os.Stdout.Write(buf[:n]) + + default: + // TCP mode: nc + if len(args) < 3 { + fmt.Fprintln(os.Stderr, "usage: nc ") + os.Exit(1) + } + conn, err := net.Dial("tcp", net.JoinHostPort(args[1], args[2])) + if err != nil { + fmt.Fprintf(os.Stderr, "nc: %s:%s: %v\n", args[1], args[2], err) + os.Exit(1) + } + defer conn.Close() + ncStream(conn) } - defer conn.Close() +} +// ncStream copies bidirectionally between conn and stdio, mirroring the +// behaviour of nc(1) in stream (TCP / unix) mode: stdin is forwarded to the +// connection and the connection's output is forwarded to stdout. Both +// directions run concurrently; ncStream returns when both copies have +// finished. +func ncStream(conn net.Conn) { var wg sync.WaitGroup wg.Add(2) go func() { @@ -470,3 +550,31 @@ func cmdNC(args []string) { }() wg.Wait() } + +// cmdHost looks up the IP addresses for a hostname and prints them in the +// format used by the standard host(1) utility from bind-utils: +// +// has address +// +// one line per address. Only A/AAAA records are printed; the tool does not +// perform reverse lookups or print NS/MX records. +// +// Usage: host +// +// Exits 0 on success. Exits 1 with a diagnostic on stderr if resolution +// fails. +func cmdHost(args []string) { + if len(args) < 2 { + fmt.Fprintln(os.Stderr, "usage: host ") + os.Exit(1) + } + name := args[1] + addrs, err := net.LookupHost(name) + if err != nil { + fmt.Fprintf(os.Stderr, "host: %s: %v\n", name, err) + os.Exit(1) + } + for _, a := range addrs { + fmt.Printf("%s has address %s\n", name, a) + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 01387a8c..cf8c43c0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -145,7 +145,7 @@ github.com/containerd/platforms ## explicit; go 1.22 github.com/containerd/plugin github.com/containerd/plugin/registry -# github.com/containerd/shimtest v0.2.1 +# github.com/containerd/shimtest v0.3.0 ## explicit; go 1.26.3 github.com/containerd/shimtest github.com/containerd/shimtest/internal/transfer From 30145dd95a6ffffb2bf1fb9bf735d8187e084cc1 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 8 Jul 2026 22:00:27 -0700 Subject: [PATCH 2/5] Update resolution of the resolv conf path Signed-off-by: Derek McGowan --- internal/shim/task/ctrnetworking.go | 56 +++- internal/shim/task/ctrnetworking_test.go | 369 +++++++++++++++++++++++ internal/shim/task/networking_test.go | 322 ++++++++++++++++++++ 3 files changed, 746 insertions(+), 1 deletion(-) create mode 100644 internal/shim/task/ctrnetworking_test.go create mode 100644 internal/shim/task/networking_test.go diff --git a/internal/shim/task/ctrnetworking.go b/internal/shim/task/ctrnetworking.go index 827684ba..68611e7d 100644 --- a/internal/shim/task/ctrnetworking.go +++ b/internal/shim/task/ctrnetworking.go @@ -189,7 +189,7 @@ func addResolvConf(ctx context.Context, b *bundle.Bundle, fallbackToHostRC bool) rcBytes = rcBuf.Bytes() } else if fallbackToHostRC { // Try giving the VM a copy of the host's resolv.conf. - if c, err := os.ReadFile("/etc/resolv.conf"); err == nil { + if c, err := os.ReadFile(hostResolvConfPath()); err == nil { rcBytes = c } } @@ -209,3 +209,57 @@ func addResolvConf(ctx context.Context, b *bundle.Bundle, fallbackToHostRC bool) }) return nil } + +// systemdResolvedFullRC is the "full" resolv.conf systemd-resolved maintains +// alongside its stub file, listing the actual upstream DNS servers rather +// than the stub's loopback listener. See resolv.conf(5) / +// systemd-resolved.service(8). +const systemdResolvedFullRC = "/run/systemd/resolve/resolv.conf" + +// hostResolvConfPath returns the host-side resolv.conf path to copy into a +// container's guest environment. +// +// Many Linux distributions symlink /etc/resolv.conf to systemd-resolved's +// stub file, whose sole nameserver is a loopback address (127.0.0.53) that +// only systemd-resolved's own stub listener answers on the host. Copying +// that verbatim into an isolated environment (a container network +// namespace, or — as here — a VM) is not useful: nothing answers on that +// loopback address there, so DNS queries fail even though the host itself +// resolves names correctly. Docker and containerd's CRI implementation +// handle this exact case the same way: prefer the "full" resolv.conf +// systemd-resolved also maintains, which lists the real upstream +// nameservers, over the stub file. This is not specific to nerdbox or to +// any particular guest network mechanism — it is a general consequence of +// copying host DNS configuration into an isolated environment. +func hostResolvConfPath() string { + const hostRC = "/etc/resolv.conf" + data, err := os.ReadFile(hostRC) + if err != nil || !onlyLoopbackNameservers(data) { + return hostRC + } + if _, err := os.Stat(systemdResolvedFullRC); err == nil { + return systemdResolvedFullRC + } + return hostRC +} + +// onlyLoopbackNameservers reports whether every "nameserver" line in a +// resolv.conf file's contents resolves to a loopback address. Returns false +// if there are no nameserver lines at all (nothing to prefer an alternative +// over). +func onlyLoopbackNameservers(resolvConf []byte) bool { + found := false + for _, line := range strings.Split(string(resolvConf), "\n") { + line = strings.TrimSpace(line) + fields := strings.Fields(line) + if len(fields) < 2 || fields[0] != "nameserver" { + continue + } + ip := net.ParseIP(fields[1]) + if ip == nil || !ip.IsLoopback() { + return false + } + found = true + } + return found +} diff --git a/internal/shim/task/ctrnetworking_test.go b/internal/shim/task/ctrnetworking_test.go new file mode 100644 index 00000000..41ef4568 --- /dev/null +++ b/internal/shim/task/ctrnetworking_test.go @@ -0,0 +1,369 @@ +/* + 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" + "encoding/json" + "net/netip" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/containerd/nerdbox/internal/nwcfg" + "github.com/containerd/nerdbox/internal/shim/task/bundle" +) + +func mustAddr(t *testing.T, s string) netip.Addr { + t.Helper() + a, err := netip.ParseAddr(s) + require.NoError(t, err) + return a +} + +// loadTestBundle writes spec to a temporary bundle directory and loads it +// via bundle.Load, so that b.AddExtraFile/b.Files() work correctly (the +// extraFiles map is only initialized by Load, not by a bare struct literal). +func loadTestBundle(t *testing.T, spec specs.Spec) *bundle.Bundle { + t.Helper() + if spec.Root == nil { + spec.Root = &specs.Root{Path: "rootfs"} + } + dir := t.TempDir() + data, err := json.Marshal(spec) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), data, 0o644)) + b, err := bundle.Load(context.Background(), dir) + require.NoError(t, err) + return b +} + +func TestParseCtrNetwork(t *testing.T) { + testcases := []struct { + name string + annotation string + wantErr string + want nwcfg.Network + }{ + { + name: "minimal valid: vmmac only", + annotation: "vmmac=fa:43:25:5d:6f:b4", + want: nwcfg.Network{VmMAC: "fa:43:25:5d:6f:b4"}, + }, + { + name: "missing vmmac", + annotation: "mac=fa:43:25:5d:6f:b5", + wantErr: "'vmmac' is missing", + }, + { + name: "invalid vmmac", + annotation: "vmmac=not-a-mac", + wantErr: "parsing MAC address", + }, + { + name: "multicast vmmac rejected", + annotation: "vmmac=ff:43:25:5d:6f:b4", + wantErr: "multicast bit is set", + }, + { + name: "duplicate vmmac rejected", + annotation: "vmmac=fa:43:25:5d:6f:b4,vmmac=fa:43:25:5d:6f:b5", + wantErr: "multiple VM MAC addresses specified", + }, + { + name: "container mac", + annotation: "vmmac=fa:43:25:5d:6f:b4,mac=fa:43:25:5d:6f:b5", + want: nwcfg.Network{ + VmMAC: "fa:43:25:5d:6f:b4", + MAC: "fa:43:25:5d:6f:b5", + }, + }, + { + name: "invalid container mac", + annotation: "vmmac=fa:43:25:5d:6f:b4,mac=not-a-mac", + wantErr: "parsing container MAC address", + }, + { + name: "multicast container mac rejected", + annotation: "vmmac=fa:43:25:5d:6f:b4,mac=ff:43:25:5d:6f:b4", + wantErr: "multicast bit is set", + }, + { + name: "duplicate container mac rejected", + annotation: "vmmac=fa:43:25:5d:6f:b4,mac=fa:43:25:5d:6f:b5,mac=fa:43:25:5d:6f:b6", + wantErr: "multiple container MAC addresses specified", + }, + { + name: "container address", + annotation: "vmmac=fa:43:25:5d:6f:b4,addr=192.168.1.2/24", + want: nwcfg.Network{ + VmMAC: "fa:43:25:5d:6f:b4", + Addrs: []netip.Prefix{mustPrefix(t, "192.168.1.2/24")}, + }, + }, + { + name: "invalid container address", + annotation: "vmmac=fa:43:25:5d:6f:b4,addr=not-an-addr", + wantErr: "parsing container address", + }, + { + name: "ipv4 gateway", + annotation: "vmmac=fa:43:25:5d:6f:b4,gw=192.168.1.1", + want: nwcfg.Network{ + VmMAC: "fa:43:25:5d:6f:b4", + DefaultGw4: mustAddr(t, "192.168.1.1"), + }, + }, + { + name: "ipv6 gateway", + annotation: "vmmac=fa:43:25:5d:6f:b4,gw=fd00::1", + want: nwcfg.Network{ + VmMAC: "fa:43:25:5d:6f:b4", + DefaultGw6: mustAddr(t, "fd00::1"), + }, + }, + { + name: "invalid gateway", + annotation: "vmmac=fa:43:25:5d:6f:b4,gw=not-an-addr", + wantErr: "parsing gateway address", + }, + { + name: "duplicate ipv4 gateway rejected", + annotation: "vmmac=fa:43:25:5d:6f:b4,gw=192.168.1.1,gw=192.168.1.2", + wantErr: "multiple IPv4 gateways specified", + }, + { + name: "duplicate ipv6 gateway rejected", + annotation: "vmmac=fa:43:25:5d:6f:b4,gw=fd00::1,gw=fd00::2", + wantErr: "multiple IPv6 gateways specified", + }, + { + name: "interface name", + annotation: "vmmac=fa:43:25:5d:6f:b4,ifname=eth1", + want: nwcfg.Network{ + VmMAC: "fa:43:25:5d:6f:b4", + IfName: "eth1", + }, + }, + { + name: "interface name too long", + annotation: "vmmac=fa:43:25:5d:6f:b4,ifname=" + strings.Repeat("x", ifNameSize), + wantErr: "interface name has more than", + }, + { + name: "unknown field rejected", + annotation: "vmmac=fa:43:25:5d:6f:b4,bogus=1", + wantErr: "unknown network field: bogus", + }, + { + name: "malformed field (no equals)", + annotation: "vmmac", + wantErr: "invalid container network field: vmmac", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + got, err := parseCtrNetwork(tc.annotation) + if tc.wantErr != "" { + require.Error(t, err) + assert.ErrorContains(t, err, tc.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +// TestCtrNetConfigFromBundle verifies that fromBundle parses every +// io.containerd.nerdbox.ctr.network.* annotation, strips them from the spec +// sent to the guest, and reports parse errors from malformed annotations. +func TestCtrNetConfigFromBundle(t *testing.T) { + t.Run("no annotations is a no-op", func(t *testing.T) { + b := &bundle.Bundle{Spec: specs.Spec{}} + var c ctrNetConfig + require.NoError(t, c.fromBundle(context.Background(), b)) + assert.Empty(t, c.Networks) + }) + + t.Run("parses annotation and strips it", func(t *testing.T) { + b := &bundle.Bundle{Spec: specs.Spec{Annotations: map[string]string{ + "io.containerd.nerdbox.ctr.network.0": "vmmac=fa:43:25:5d:6f:b4", + "io.containerd.nerdbox.other": "unrelated", + }}} + var c ctrNetConfig + require.NoError(t, c.fromBundle(context.Background(), b)) + require.Len(t, c.Networks, 1) + assert.Equal(t, "fa:43:25:5d:6f:b4", c.Networks[0].VmMAC) + _, hasNetworkAnnot := b.Spec.Annotations["io.containerd.nerdbox.ctr.network.0"] + assert.False(t, hasNetworkAnnot) + _, hasOther := b.Spec.Annotations["io.containerd.nerdbox.other"] + assert.True(t, hasOther) + }) + + t.Run("propagates parse errors", func(t *testing.T) { + b := &bundle.Bundle{Spec: specs.Spec{Annotations: map[string]string{ + "io.containerd.nerdbox.ctr.network.0": "mac=not-a-mac", + }}} + var c ctrNetConfig + err := c.fromBundle(context.Background(), b) + require.Error(t, err) + assert.ErrorContains(t, err, "failed to parse container network annotation") + }) +} + +// TestAddResolvConf covers the three resolv.conf sources handled by +// addResolvConf: an explicit DNS annotation, a fallback to the host's +// /etc/resolv.conf, and skipping when the bundle already mounts one. +func TestAddResolvConf(t *testing.T) { + t.Run("existing resolv.conf mount is left untouched", func(t *testing.T) { + b := &bundle.Bundle{Spec: specs.Spec{Mounts: []specs.Mount{ + {Destination: "/etc/resolv.conf", Type: "bind", Source: "/custom/resolv.conf"}, + }}} + require.NoError(t, addResolvConf(context.Background(), b, true)) + require.Len(t, b.Spec.Mounts, 1) + assert.Equal(t, "/custom/resolv.conf", b.Spec.Mounts[0].Source) + }) + + t.Run("dns annotation generates resolv.conf content", func(t *testing.T) { + b := loadTestBundle(t, specs.Spec{Annotations: map[string]string{ + "io.containerd.nerdbox.ctr.dns": "nameserver=8.8.8.8,search=example.com", + }}) + require.NoError(t, addResolvConf(context.Background(), b, false)) + + // Annotation is stripped after being consumed. + _, hasAnnot := b.Spec.Annotations["io.containerd.nerdbox.ctr.dns"] + assert.False(t, hasAnnot) + + require.Len(t, b.Spec.Mounts, 1) + assert.Equal(t, "/etc/resolv.conf", b.Spec.Mounts[0].Destination) + assert.Equal(t, "resolv.conf", b.Spec.Mounts[0].Source) + + files, err := b.Files() + require.NoError(t, err) + content := string(files["resolv.conf"]) + assert.Contains(t, content, "nameserver 8.8.8.8\n") + assert.Contains(t, content, "search example.com\n") + }) + + t.Run("fallback to host resolv.conf when no annotation and fallback enabled", func(t *testing.T) { + // addResolvConf reads the fixed path "/etc/resolv.conf" on the host + // running the test. Whether or not that file exists/is readable, + // the mount destination must always be /etc/resolv.conf; the source + // is "resolv.conf" (an extra file) when the host file was read, or + // the VM's own /etc/resolv.conf when it was not. + b := loadTestBundle(t, specs.Spec{}) + require.NoError(t, addResolvConf(context.Background(), b, true)) + require.Len(t, b.Spec.Mounts, 1) + assert.Equal(t, "/etc/resolv.conf", b.Spec.Mounts[0].Destination) + assert.Contains(t, []string{"resolv.conf", "/etc/resolv.conf"}, b.Spec.Mounts[0].Source) + }) + + t.Run("no annotation and fallback disabled defaults to VM resolv.conf", func(t *testing.T) { + b := &bundle.Bundle{Spec: specs.Spec{}} + require.NoError(t, addResolvConf(context.Background(), b, false)) + require.Len(t, b.Spec.Mounts, 1) + assert.Equal(t, "/etc/resolv.conf", b.Spec.Mounts[0].Destination) + assert.Equal(t, "/etc/resolv.conf", b.Spec.Mounts[0].Source) + }) +} + +// TestOnlyLoopbackNameservers covers the resolv.conf parsing used to detect +// a systemd-resolved-style stub resolver (a nameserver line pointing at a +// loopback address) versus a resolv.conf listing real, reachable +// nameservers. +func TestOnlyLoopbackNameservers(t *testing.T) { + testcases := []struct { + name string + rc string + want bool + }{ + { + name: "single loopback nameserver (systemd-resolved stub)", + rc: "nameserver 127.0.0.53\noptions edns0 trust-ad\n", + want: true, + }, + { + name: "single real nameserver", + rc: "nameserver 8.8.8.8\n", + want: false, + }, + { + name: "mixed loopback and real nameservers is not all-loopback", + rc: "nameserver 127.0.0.53\nnameserver 1.1.1.1\n", + want: false, + }, + { + name: "multiple loopback nameservers", + rc: "nameserver 127.0.0.53\nnameserver 127.0.0.54\n", + want: true, + }, + { + name: "IPv6 loopback nameserver", + rc: "nameserver ::1\n", + want: true, + }, + { + name: "no nameserver lines at all", + rc: "search example.com\noptions edns0\n", + want: false, + }, + { + name: "empty file", + rc: "", + want: false, + }, + { + name: "malformed nameserver line ignored", + rc: "nameserver\n", + want: false, + }, + { + name: "comments and blank lines ignored", + rc: "# this is a comment\n\nnameserver 127.0.0.53\n", + want: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + got := onlyLoopbackNameservers([]byte(tc.rc)) + assert.Equal(t, tc.want, got) + }) + } +} + +// TestHostResolvConfPath verifies that hostResolvConfPath prefers +// systemd-resolved's "full" resolv.conf when /etc/resolv.conf looks like the +// loopback stub file and the full file exists, and falls back to +// /etc/resolv.conf in every other case. +func TestHostResolvConfPath(t *testing.T) { + // hostResolvConfPath reads fixed OS paths ("/etc/resolv.conf" and + // systemdResolvedFullRC), so we can only exercise the "not a stub" or + // "full file absent" fallback branches deterministically without + // mocking the filesystem. What we can assert unconditionally: the + // function always returns a non-empty path, and it only ever returns + // one of the two known candidates. + got := hostResolvConfPath() + assert.Contains(t, []string{"/etc/resolv.conf", systemdResolvedFullRC}, got) +} diff --git a/internal/shim/task/networking_test.go b/internal/shim/task/networking_test.go new file mode 100644 index 00000000..57c0c03d --- /dev/null +++ b/internal/shim/task/networking_test.go @@ -0,0 +1,322 @@ +/* + 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" + "net" + "net/netip" + "testing" + + "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/containerd/nerdbox/internal/shim/sandbox" + "github.com/containerd/nerdbox/internal/shim/task/bundle" +) + +func mustMAC(t *testing.T, s string) net.HardwareAddr { + t.Helper() + mac, err := net.ParseMAC(s) + require.NoError(t, err) + return mac +} + +func mustPrefix(t *testing.T, s string) netip.Prefix { + t.Helper() + p, err := netip.ParsePrefix(s) + require.NoError(t, err) + return p +} + +func TestParseNetwork(t *testing.T) { + testcases := []struct { + name string + annotation string + wantErr string + want network + }{ + { + name: "minimal valid: socket, mode, mac", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4", + want: network{ + endpoint: "/tmp/net.sock", + mode: "unixgram", + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + }, + }, + { + name: "unixstream mode", + annotation: "socket=/tmp/net.sock,mode=unixstream,mac=fa:43:25:5d:6f:b4", + want: network{ + endpoint: "/tmp/net.sock", + mode: "unixstream", + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + }, + }, + { + name: "invalid mode", + annotation: "socket=/tmp/net.sock,mode=tcp,mac=fa:43:25:5d:6f:b4", + wantErr: "invalid network mode: tcp", + }, + { + name: "invalid mac", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=not-a-mac", + wantErr: "parsing MAC address", + }, + { + name: "multicast mac rejected", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=ff:43:25:5d:6f:b4", + wantErr: "multicast bit is set", + }, + { + name: "dhcp true", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,dhcp=true", + want: network{ + endpoint: "/tmp/net.sock", + mode: "unixgram", + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + dhcp: true, + }, + }, + { + name: "invalid dhcp value", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,dhcp=maybe", + wantErr: "parsing DHCP field", + }, + { + name: "ipv4 address", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,addr=192.168.127.2/24", + want: network{ + endpoint: "/tmp/net.sock", + mode: "unixgram", + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + addr4: mustPrefix(t, "192.168.127.2/24"), + }, + }, + { + name: "ipv4 and ipv6 addresses", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,addr=192.168.127.2/24,addr=fd00::2/64", + want: network{ + endpoint: "/tmp/net.sock", + mode: "unixgram", + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + addr4: mustPrefix(t, "192.168.127.2/24"), + addr6: mustPrefix(t, "fd00::2/64"), + }, + }, + { + name: "duplicate ipv4 address rejected", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,addr=192.168.127.2/24,addr=192.168.127.3/24", + wantErr: "multiple IPv4 addresses specified", + }, + { + name: "duplicate ipv6 address rejected", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,addr=fd00::2/64,addr=fd00::3/64", + wantErr: "multiple IPv6 addresses specified", + }, + { + name: "invalid address", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,addr=not-an-addr", + wantErr: "parsing address", + }, + { + name: "vfkit true", + annotation: "socket=/tmp/net.sock,mode=unixstream,mac=fa:43:25:5d:6f:b4,vfkit=true", + want: network{ + endpoint: "/tmp/net.sock", + mode: "unixstream", + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + vfkit: true, + }, + }, + { + name: "invalid vfkit value", + annotation: "socket=/tmp/net.sock,mode=unixstream,mac=fa:43:25:5d:6f:b4,vfkit=maybe", + wantErr: "parsing vfkit field", + }, + { + name: "vnet_hdr true", + annotation: "socket=/tmp/net.sock,mode=unixstream,mac=fa:43:25:5d:6f:b4,vnet_hdr=true", + want: network{ + endpoint: "/tmp/net.sock", + mode: "unixstream", + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + vnetHdr: true, + }, + }, + { + name: "invalid vnet_hdr value", + annotation: "socket=/tmp/net.sock,mode=unixstream,mac=fa:43:25:5d:6f:b4,vnet_hdr=maybe", + wantErr: "parsing vnet_hdr field", + }, + { + name: "features parsed", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,features=VIRTIO_NET_F_CSUM", + want: network{ + endpoint: "/tmp/net.sock", + mode: "unixgram", + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + features: 1 << 0, + }, + }, + { + name: "invalid features value", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,features=NOT_A_FEATURE", + wantErr: "parsing features", + }, + { + name: "unknown field rejected", + annotation: "socket=/tmp/net.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4,bogus=1", + wantErr: "unknown network field: bogus", + }, + { + name: "malformed field (no equals)", + annotation: "socket=/tmp/net.sock,mode", + wantErr: "invalid network field: mode", + }, + { + name: "missing socket", + annotation: "mode=unixgram,mac=fa:43:25:5d:6f:b4", + wantErr: "missing required field(s)", + }, + { + name: "missing mode", + annotation: "socket=/tmp/net.sock,mac=fa:43:25:5d:6f:b4", + wantErr: "missing required field(s)", + }, + { + name: "missing mac", + annotation: "socket=/tmp/net.sock,mode=unixgram", + wantErr: "missing required field(s)", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + got, err := parseNetwork(tc.annotation) + if tc.wantErr != "" { + require.Error(t, err) + assert.ErrorContains(t, err, tc.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want.endpoint, got.endpoint) + assert.Equal(t, tc.want.mode, got.mode) + assert.Equal(t, tc.want.mac, got.mac) + assert.Equal(t, tc.want.dhcp, got.dhcp) + assert.Equal(t, tc.want.addr4, got.addr4) + assert.Equal(t, tc.want.addr6, got.addr6) + assert.Equal(t, tc.want.features, got.features) + assert.Equal(t, tc.want.vfkit, got.vfkit) + assert.Equal(t, tc.want.vnetHdr, got.vnetHdr) + }) + } +} + +// TestNetworksProviderFromBundle verifies that FromBundle parses every +// io.containerd.nerdbox.network.* annotation, strips them from the spec sent +// to the guest, and reports parse errors from malformed annotations. +func TestNetworksProviderFromBundle(t *testing.T) { + t.Run("no annotations is a no-op", func(t *testing.T) { + b := &bundle.Bundle{Spec: specs.Spec{}} + var p networksProvider + require.NoError(t, p.FromBundle(context.Background(), b)) + assert.Empty(t, p.nws) + }) + + t.Run("parses multiple network annotations and strips them", func(t *testing.T) { + b := &bundle.Bundle{Spec: specs.Spec{Annotations: map[string]string{ + "io.containerd.nerdbox.network.0": "socket=/tmp/a.sock,mode=unixgram,mac=fa:43:25:5d:6f:b4", + "io.containerd.nerdbox.other": "unrelated", + }}} + var p networksProvider + require.NoError(t, p.FromBundle(context.Background(), b)) + require.Len(t, p.nws, 1) + assert.Equal(t, "/tmp/a.sock", p.nws[0].endpoint) + // Only the matching annotation is stripped. + _, hasNetworkAnnot := b.Spec.Annotations["io.containerd.nerdbox.network.0"] + assert.False(t, hasNetworkAnnot) + _, hasOther := b.Spec.Annotations["io.containerd.nerdbox.other"] + assert.True(t, hasOther) + }) + + t.Run("propagates parse errors", func(t *testing.T) { + b := &bundle.Bundle{Spec: specs.Spec{Annotations: map[string]string{ + "io.containerd.nerdbox.network.0": "mode=bogus", + }}} + var p networksProvider + err := p.FromBundle(context.Background(), b) + require.Error(t, err) + assert.ErrorContains(t, err, "failed to parse network annotation") + }) +} + +// TestNetworksProviderSandboxOptions verifies that SandboxOptions translates +// parsed network annotations into the correct sandbox.WithNIC options, +// including mode selection and flag composition. +func TestNetworksProviderSandboxOptions(t *testing.T) { + p := networksProvider{nws: []network{ + { + endpoint: "/tmp/a.sock", + mode: "unixstream", + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + vfkit: true, + vnetHdr: true, + }, + { + endpoint: "/tmp/b.sock", + mode: "unixgram", + mac: mustMAC(t, "fa:43:25:5d:6f:b5"), + }, + }} + + opts := p.SandboxOptions() + require.Len(t, opts, 2) + + var o sandbox.Options + for _, opt := range opts { + opt(&o) + } + + require.Len(t, o.NICs, 2) + assert.Equal(t, "/tmp/a.sock", o.NICs[0].Endpoint) + assert.Equal(t, uint32(NET_FLAG_VFKIT|NET_FLAG_INCLUDE_VNET_HEADER), o.NICs[0].Flags) + assert.Equal(t, "/tmp/b.sock", o.NICs[1].Endpoint) + assert.Equal(t, uint32(0), o.NICs[1].Flags) +} + +// TestNetworksProviderInitArgs verifies that InitArgs formats the guest +// network configuration string with the expected fields and ordering. +func TestNetworksProviderInitArgs(t *testing.T) { + p := networksProvider{nws: []network{ + { + mac: mustMAC(t, "fa:43:25:5d:6f:b4"), + dhcp: true, + addr4: mustPrefix(t, "192.168.127.2/24"), + }, + { + mac: mustMAC(t, "fa:43:25:5d:6f:b5"), + }, + }} + + args := p.InitArgs() + require.Len(t, args, 2) + assert.Equal(t, "-network=mac=fa:43:25:5d:6f:b4,dhcp=true,addr=192.168.127.2/24", args[0]) + assert.Equal(t, "-network=mac=fa:43:25:5d:6f:b5", args[1]) +} From e4a2a899e24dd663412c0b6681185cd0f15b530a Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Wed, 8 Jul 2026 22:01:05 -0700 Subject: [PATCH 3/5] Update TSI patches for latest libkrun TSI version Signed-off-by: Derek McGowan --- .gitattributes | 1 + Dockerfile | 3 +- ...-vsock-Add-support-for-SIOCINQ-ioctl.patch | 55 ++ ...o-vsock-implement-has_data-for-DGRAM.patch | 133 +++ ...Socket-Impersonation-implementation.patch} | 811 ++++++++++++------ ...-allow-hijacking-sockets-tsi_hijack.patch} | 46 +- 6 files changed, 784 insertions(+), 265 deletions(-) create mode 100644 kernel/patches/0009-vsock-Add-support-for-SIOCINQ-ioctl.patch create mode 100644 kernel/patches/0010-virtio-vsock-implement-has_data-for-DGRAM.patch rename kernel/patches/{0009-Transparent-Socket-Impersonation-implementation.patch => 0011-Transparent-Socket-Impersonation-implementation.patch} (62%) rename kernel/patches/{0010-tsi-allow-hijacking-sockets-tsi_hijack.patch => 0012-tsi-allow-hijacking-sockets-tsi_hijack.patch} (52%) diff --git a/.gitattributes b/.gitattributes index 16de697f..5c2a4f24 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ Dockerfile text eol=lf +kernel/patches/*.patch -whitespace diff --git a/Dockerfile b/Dockerfile index 9043e177..a456cb2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,8 +94,9 @@ COPY kernel/config-${KERNEL_VERSION}-${KERNEL_ARCH} /usr/src/linux/.config COPY kernel/patches /usr/src/linux/patches RUN < +Date: Tue, 8 Jul 2025 14:36:12 +0800 +Subject: [PATCH 09/30] vsock: Add support for SIOCINQ ioctl + +Add support for SIOCINQ ioctl, indicating the length of bytes unread in the +socket. The value is obtained from `vsock_stream_has_data()`. + +Signed-off-by: Xuewei Niu +Reviewed-by: Stefano Garzarella +Reviewed-by: Luigi Leonardi +Link: https://patch.msgid.link/20250708-siocinq-v6-2-3775f9a9e359@antgroup.com +Signed-off-by: Jakub Kicinski +(cherry picked from commit f7c72265927540fb24c99fee8a54da7db537656c) +Signed-off-by: Sergio Lopez +--- + net/vmw_vsock/af_vsock.c | 22 ++++++++++++++++++++++ + 1 file changed, 22 insertions(+) + +diff --git a/net/vmw_vsock/af_vsock.c b/net/vmw_vsock/af_vsock.c +index 7c9c8696a07a..3b420d8f0c84 100644 +--- a/net/vmw_vsock/af_vsock.c ++++ b/net/vmw_vsock/af_vsock.c +@@ -1545,6 +1545,28 @@ static int vsock_do_ioctl(struct socket *sock, unsigned int cmd, + vsk = vsock_sk(sk); + + switch (cmd) { ++ case SIOCINQ: { ++ ssize_t n_bytes; ++ ++ if (!vsk->transport) { ++ ret = -EOPNOTSUPP; ++ break; ++ } ++ ++ if (sock_type_connectible(sk->sk_type) && ++ sk->sk_state == TCP_LISTEN) { ++ ret = -EINVAL; ++ break; ++ } ++ ++ n_bytes = vsock_stream_has_data(vsk); ++ if (n_bytes < 0) { ++ ret = n_bytes; ++ break; ++ } ++ ret = put_user(n_bytes, arg); ++ break; ++ } + case SIOCOUTQ: { + ssize_t n_bytes; + +-- +2.54.0 + diff --git a/kernel/patches/0010-virtio-vsock-implement-has_data-for-DGRAM.patch b/kernel/patches/0010-virtio-vsock-implement-has_data-for-DGRAM.patch new file mode 100644 index 00000000..dff41447 --- /dev/null +++ b/kernel/patches/0010-virtio-vsock-implement-has_data-for-DGRAM.patch @@ -0,0 +1,133 @@ +From adc7bebfd1e4dff1a2160fb587fc534f38463cba Mon Sep 17 00:00:00 2001 +From: Sergio Lopez +Date: Thu, 28 May 2026 11:43:46 +0200 +Subject: [PATCH 10/30] virtio/vsock: implement has_data for DGRAM + +Signed-off-by: Sergio Lopez +--- + include/linux/virtio_vsock.h | 1 + + include/net/af_vsock.h | 2 ++ + net/vmw_vsock/af_vsock.c | 15 ++++++++++++++- + net/vmw_vsock/virtio_transport.c | 1 + + net/vmw_vsock/virtio_transport_common.c | 15 +++++++++++++++ + net/vmw_vsock/vsock_loopback.c | 1 + + 6 files changed, 34 insertions(+), 1 deletion(-) + +diff --git a/include/linux/virtio_vsock.h b/include/linux/virtio_vsock.h +index e3d7afa29894..34220effc4b0 100644 +--- a/include/linux/virtio_vsock.h ++++ b/include/linux/virtio_vsock.h +@@ -215,6 +215,7 @@ ssize_t + virtio_transport_seqpacket_dequeue(struct vsock_sock *vsk, + struct msghdr *msg, + int flags); ++s64 virtio_transport_dgram_has_data(struct vsock_sock *vsk); + s64 virtio_transport_stream_has_data(struct vsock_sock *vsk); + s64 virtio_transport_stream_has_space(struct vsock_sock *vsk); + u32 virtio_transport_seqpacket_has_data(struct vsock_sock *vsk); +diff --git a/include/net/af_vsock.h b/include/net/af_vsock.h +index 5f93bb290a83..45469aa8cf60 100644 +--- a/include/net/af_vsock.h ++++ b/include/net/af_vsock.h +@@ -76,6 +76,7 @@ struct vsock_sock { + }; + + s64 vsock_connectible_has_data(struct vsock_sock *vsk); ++s64 vsock_dgram_has_data(struct vsock_sock *vsk); + s64 vsock_stream_has_data(struct vsock_sock *vsk); + s64 vsock_stream_has_space(struct vsock_sock *vsk); + struct sock *vsock_create_connected(struct sock *parent); +@@ -134,6 +135,7 @@ struct vsock_transport { + * header. + */ + const size_t dgram_payload_offset; ++ s64 (*dgram_has_data)(struct vsock_sock *); + + /* STREAM. */ + /* TODO: stream_bind() */ +diff --git a/net/vmw_vsock/af_vsock.c b/net/vmw_vsock/af_vsock.c +index 3b420d8f0c84..73e2eac4c3af 100644 +--- a/net/vmw_vsock/af_vsock.c ++++ b/net/vmw_vsock/af_vsock.c +@@ -1007,6 +1007,15 @@ s64 vsock_stream_has_data(struct vsock_sock *vsk) + } + EXPORT_SYMBOL_GPL(vsock_stream_has_data); + ++s64 vsock_dgram_has_data(struct vsock_sock *vsk) ++{ ++ if (WARN_ON(!vsk->transport)) ++ return 0; ++ ++ return vsk->transport->dgram_has_data(vsk); ++} ++EXPORT_SYMBOL_GPL(vsock_dgram_has_data); ++ + s64 vsock_connectible_has_data(struct vsock_sock *vsk) + { + struct sock *sk = sk_vsock(vsk); +@@ -1559,7 +1568,11 @@ static int vsock_do_ioctl(struct socket *sock, unsigned int cmd, + break; + } + +- n_bytes = vsock_stream_has_data(vsk); ++ if (sk->sk_type == SOCK_DGRAM) ++ n_bytes = vsock_dgram_has_data(vsk); ++ else ++ n_bytes = vsock_stream_has_data(vsk); ++ + if (n_bytes < 0) { + ret = n_bytes; + break; +diff --git a/net/vmw_vsock/virtio_transport.c b/net/vmw_vsock/virtio_transport.c +index 58d15edd296e..43eeeb32a318 100644 +--- a/net/vmw_vsock/virtio_transport.c ++++ b/net/vmw_vsock/virtio_transport.c +@@ -558,6 +558,7 @@ static struct virtio_transport virtio_transport = { + .dgram_get_cid = virtio_transport_dgram_get_cid, + .dgram_get_port = virtio_transport_dgram_get_port, + .dgram_get_length = virtio_transport_dgram_get_length, ++ .dgram_has_data = virtio_transport_dgram_has_data, + + .stream_dequeue = virtio_transport_stream_dequeue, + .stream_enqueue = virtio_transport_stream_enqueue, +diff --git a/net/vmw_vsock/virtio_transport_common.c b/net/vmw_vsock/virtio_transport_common.c +index a7e9bcc49a8e..6498ab12ce31 100644 +--- a/net/vmw_vsock/virtio_transport_common.c ++++ b/net/vmw_vsock/virtio_transport_common.c +@@ -862,6 +862,21 @@ int virtio_transport_dgram_get_length(struct sk_buff *skb, size_t *len) + } + EXPORT_SYMBOL_GPL(virtio_transport_dgram_get_length); + ++s64 virtio_transport_dgram_has_data(struct vsock_sock *vsk) ++{ ++ struct sock *sk = sk_vsock(vsk); ++ struct sk_buff *skb; ++ s64 bytes = 0; ++ ++ spin_lock_bh(&sk->sk_receive_queue.lock); ++ skb_queue_walk(&sk->sk_receive_queue, skb) ++ bytes += skb->len; ++ spin_unlock_bh(&sk->sk_receive_queue.lock); ++ ++ return bytes; ++} ++EXPORT_SYMBOL_GPL(virtio_transport_dgram_has_data); ++ + s64 virtio_transport_stream_has_data(struct vsock_sock *vsk) + { + struct virtio_vsock_sock *vvs = vsk->trans; +diff --git a/net/vmw_vsock/vsock_loopback.c b/net/vmw_vsock/vsock_loopback.c +index b3066c854bb9..09e6162030b0 100644 +--- a/net/vmw_vsock/vsock_loopback.c ++++ b/net/vmw_vsock/vsock_loopback.c +@@ -71,6 +71,7 @@ static struct virtio_transport loopback_transport = { + .dgram_get_cid = virtio_transport_dgram_get_cid, + .dgram_get_port = virtio_transport_dgram_get_port, + .dgram_get_length = virtio_transport_dgram_get_length, ++ .dgram_has_data = virtio_transport_dgram_has_data, + + .stream_dequeue = virtio_transport_stream_dequeue, + .stream_enqueue = virtio_transport_stream_enqueue, +-- +2.54.0 + diff --git a/kernel/patches/0009-Transparent-Socket-Impersonation-implementation.patch b/kernel/patches/0011-Transparent-Socket-Impersonation-implementation.patch similarity index 62% rename from kernel/patches/0009-Transparent-Socket-Impersonation-implementation.patch rename to kernel/patches/0011-Transparent-Socket-Impersonation-implementation.patch index 2f5a656b..8c88062f 100644 --- a/kernel/patches/0009-Transparent-Socket-Impersonation-implementation.patch +++ b/kernel/patches/0011-Transparent-Socket-Impersonation-implementation.patch @@ -1,16 +1,16 @@ -From 8b6e0c7550ba7f4c11e6df61c48ed0783dc38513 Mon Sep 17 00:00:00 2001 +From 6ec974c7d185814ca12661e8081dcd4cb4ae179b Mon Sep 17 00:00:00 2001 From: Sergio Lopez Date: Thu, 19 May 2022 22:38:26 +0200 -Subject: [PATCH 09/21] Transparent Socket Impersonation implementation +Subject: [PATCH 11/30] Transparent Socket Impersonation implementation Transparent Socket Impersonation (AF_TSI) is an address family that -provides sockets presenting two simultaneous personalities, AF_INET -and AF_VSOCK. +provides sockets presenting two simultaneous personalities, one of +AF_INET, AF_INET6 or AF_UNIX, and AF_VSOCK. -By using this an AF_TSI socket, the kernel can impersonate an AF_INET -socket (only SOCK_STREAM and SOCK_DGRAM are supported at the moment) -with an AF_VSOCK one, allowing all communications to happen through a -vsock device. +By using this an AF_TSI socket, the kernel can impersonate an +AF_INET, AF_INET6 or AF_UNIX socket (only SOCK_STREAM and SOCK_DGRAM +are supported at the moment) with an AF_VSOCK one, allowing all +communications to happen through a vsock device. This strategy is specially useful for microVMs, as it allows the VMM (Virtual Machine Monitor) to provide network connectivity to the @@ -20,44 +20,51 @@ low overall footprint. TODO - implement remote [get|set]sockopt +Co-authored-by: Matej Hrica + Signed-off-by: Sergio Lopez +Signed-off-by: Matej Hrica --- - include/linux/socket.h | 4 +- + include/linux/socket.h | 8 +- net/Kconfig | 1 + net/Makefile | 1 + - net/socket.c | 1 + + net/socket.c | 3 + net/tsi/Kconfig | 7 + net/tsi/Makefile | 4 + - net/tsi/af_tsi.c | 1280 +++++++++++++++++++++++++++ - net/tsi/af_tsi.h | 100 +++ - security/selinux/hooks.c | 4 +- + net/tsi/af_tsi.c | 1577 +++++++++++++++++++++++++++ + net/tsi/af_tsi.h | 107 ++ + security/selinux/hooks.c | 8 +- security/selinux/include/classmap.h | 3 +- - 10 files changed, 1402 insertions(+), 3 deletions(-) + 10 files changed, 1716 insertions(+), 3 deletions(-) create mode 100644 net/tsi/Kconfig create mode 100644 net/tsi/Makefile create mode 100644 net/tsi/af_tsi.c create mode 100644 net/tsi/af_tsi.h diff --git a/include/linux/socket.h b/include/linux/socket.h -index c3322eb3d686..77b7e8246102 100644 +index c3322eb3d686..5ae7f94b3abc 100644 --- a/include/linux/socket.h +++ b/include/linux/socket.h -@@ -240,8 +240,9 @@ struct ucred { +@@ -240,8 +240,11 @@ struct ucred { #define AF_MCTP 45 /* Management component * transport protocol */ -+#define AF_TSI 46 /* TSI sockets */ ++#define AF_TSI 46 /* TSI for IPv4 sockets */ ++#define AF_TSI6 47 /* TSI for IPv6 sockets */ ++#define AF_TSIU 48 /* TSI for UNIX sockets */ -#define AF_MAX 46 /* For now.. */ -+#define AF_MAX 47 /* For now.. */ ++#define AF_MAX 49 /* For now.. */ /* Protocol families, same as address families. */ #define PF_UNSPEC AF_UNSPEC -@@ -292,6 +293,7 @@ struct ucred { +@@ -292,6 +295,9 @@ struct ucred { #define PF_SMC AF_SMC #define PF_XDP AF_XDP #define PF_MCTP AF_MCTP +#define PF_TSI AF_TSI ++#define PF_TSI6 AF_TSI6 ++#define PF_TSIU AF_TSIU #define PF_MAX AF_MAX /* Maximum queue length specifiable by listen. */ @@ -83,14 +90,16 @@ index 65bb8c72a35e..c1db937f3212 100644 obj-$(CONFIG_NET_HANDSHAKE) += handshake/ +obj-$(CONFIG_TSI) += tsi/ diff --git a/net/socket.c b/net/socket.c -index 042451f01c65..5ad75d15e1ad 100644 +index 878155076bc0..a55927134851 100644 --- a/net/socket.c +++ b/net/socket.c -@@ -217,6 +217,7 @@ static const char * const pf_family_names[] = { +@@ -217,6 +217,9 @@ static const char * const pf_family_names[] = { [PF_SMC] = "PF_SMC", [PF_XDP] = "PF_XDP", [PF_MCTP] = "PF_MCTP", + [PF_TSI] = "PF_TSI", ++ [PF_TSI6] = "PF_TSI6", ++ [PF_TSIU] = "PF_TSIU", }; /* @@ -119,10 +128,10 @@ index 000000000000..8b3cf74116a5 +tsi-y := af_tsi.o diff --git a/net/tsi/af_tsi.c b/net/tsi/af_tsi.c new file mode 100644 -index 000000000000..f43a17cff3a3 +index 000000000000..3d2bcd8d2ba4 --- /dev/null +++ b/net/tsi/af_tsi.c -@@ -0,0 +1,1280 @@ +@@ -0,0 +1,1577 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Transparent Socket Impersonation Driver @@ -136,29 +145,70 @@ index 000000000000..f43a17cff3a3 +#include +#include +#include ++#include +#include ++#include ++#include ++#include ++#include ++#include +#include "af_tsi.h" + -+/* Protocol family. */ -+static struct proto tsi_proto = { ++/* TSI for IPv4 family. */ ++static struct proto tsi_in4_proto = { + .name = "AF_TSI", + .owner = THIS_MODULE, + .obj_size = sizeof(struct tsi_sock), +}; + -+#define tsi_sk(__sk) ((struct tsi_sock *)__sk) -+#define sk_tsi(__tsk) (&(__tsk)->sk) ++/* TSI for IPv6 family. */ ++static struct proto tsi_in6_proto = { ++ .name = "AF_TSI6", ++ .owner = THIS_MODULE, ++ .obj_size = sizeof(struct tsi_sock), ++}; ++ ++/* TSI for UNIX family. */ ++static struct proto tsi_un_proto = { ++ .name = "AF_TSIU", ++ .owner = THIS_MODULE, ++ .obj_size = sizeof(struct tsi_sock), ++}; ++ ++#define tsi_sk(__sk) ((struct tsi_sock *)__sk) ++#define sk_tsi(__tsk) (&(__tsk)->sk) ++ ++static int tsi_check_addr_len(struct tsi_sock *tsk, int addr_len) ++{ ++ if (tsk->family == PF_INET) { ++ if (addr_len != sizeof(struct sockaddr_in)) { ++ return -EINVAL; ++ } ++ } else if (tsk->family == PF_INET6) { ++ if (addr_len != sizeof(struct sockaddr_in6)) { ++ return -EINVAL; ++ } ++ } else if (tsk->family == PF_UNIX) { ++ if (addr_len > TSI_ADDR_LEN) { ++ return -EINVAL; ++ } ++ } else { ++ panic("Unsupported family in tsk->family=%d", tsk->family); ++ } ++ ++ return 0; ++} + +static int tsi_create_control_socket(struct socket **csocket) +{ + struct sockaddr_vm vm_addr; + int err; + -+ err = __sock_create(current->nsproxy->net_ns, PF_VSOCK, -+ SOCK_DGRAM, 0, csocket, 1); ++ err = __sock_create(current->nsproxy->net_ns, PF_VSOCK, SOCK_DGRAM, 0, ++ csocket, 1); + if (err) { + pr_debug("%s: error creating control socket\n", __func__); -+ goto release; ++ return err; + } + + memset(&vm_addr, 0, sizeof(struct sockaddr_vm)); @@ -170,21 +220,18 @@ index 000000000000..f43a17cff3a3 + sizeof(struct sockaddr_vm)); + if (err) { + pr_debug("%s: error binding port\n", __func__); -+ goto release; ++ sock_release(*csocket); ++ return err; + } + + return 0; -+ -+release: -+ (*csocket)->ops->release(*csocket); -+ return err; +} + +static int tsi_control_sendrecv_msg(struct socket *csocket, int port, + void *data, int data_len, bool recv) +{ + struct sockaddr_vm vm_addr; -+ struct msghdr msg = {.msg_flags = 0 }; ++ struct msghdr msg = { .msg_flags = 0 }; + struct kvec iov = { + .iov_base = data, + .iov_len = data_len, @@ -204,14 +251,14 @@ index 000000000000..f43a17cff3a3 + return kernel_sendmsg(csocket, &msg, &iov, 1, iov.iov_len); +} + -+static int tsi_control_sendmsg(struct socket *csocket, int port, -+ void *data, int data_len) ++static int tsi_control_sendmsg(struct socket *csocket, int port, void *data, ++ int data_len) +{ + return tsi_control_sendrecv_msg(csocket, port, data, data_len, 0); +} + -+static int tsi_control_recvmsg(struct socket *csocket, int port, -+ void *data, int data_len) ++static int tsi_control_recvmsg(struct socket *csocket, int port, void *data, ++ int data_len) +{ + return tsi_control_sendrecv_msg(csocket, port, data, data_len, 1); +} @@ -241,6 +288,8 @@ index 000000000000..f43a17cff3a3 + vsocket = tsk->vsocket; + sk = sock->sk; + ++ lock_sock(sk); ++ + pr_debug("%s: tsk=%p vsocket=%p isocket=%p\n", __func__, tsk, vsocket, + isocket); + @@ -252,26 +301,17 @@ index 000000000000..f43a17cff3a3 + tpr.svm_port = tsk->svm_port; + tpr.svm_peer_port = tsk->svm_peer_port; + -+ err = tsi_control_sendmsg(tsk->csocket, -+ TSI_PROXY_RELEASE, ++ err = tsi_control_sendmsg(tsk->csocket, TSI_PROXY_RELEASE, + (void *)&tpr, + sizeof(struct tsi_proxy_release)); + -+ err = vsocket->ops->release(vsocket); -+ if (err != 0) { -+ pr_debug("%s: error releasing vsock socket\n", -+ __func__); -+ } ++ sock_release(vsocket); + } + + if (!isocket) { + pr_debug("%s: no isocket\n", __func__); + } else { -+ err = isocket->ops->release(isocket); -+ if (err != 0) { -+ pr_debug("%s: error releasing inner socket\n", -+ __func__); -+ } ++ sock_release(isocket); + } + + sock_orphan(sk); @@ -301,6 +341,13 @@ index 000000000000..f43a17cff3a3 + + pr_debug("%s: vsocket=%p\n", __func__, vsocket); + ++ err = tsi_check_addr_len(tsk, addr_len); ++ if (err != 0) { ++ pr_debug("%s: invalid addr_len: %d\n", __func__, addr_len); ++ err = -EINVAL; ++ goto release; ++ } ++ + if (!isocket) { + pr_debug("%s: no isocket\n", __func__); + err = -EINVAL; @@ -329,14 +376,15 @@ index 000000000000..f43a17cff3a3 + if (err) { + pr_debug("%s: error setting up vsock listener: %d\n", __func__, + err); -+ } else if (addr_len >= sizeof(struct sockaddr_in)) { -+ if (!tsk->bound_addr) { -+ tsk->bound_addr = -+ kmalloc(sizeof(struct sockaddr_in), GFP_KERNEL); -+ } -+ memcpy(tsk->bound_addr, addr, sizeof(struct sockaddr_in)); ++ goto release; + } + ++ if (!tsk->bound_addr) { ++ tsk->bound_addr = kmalloc(addr_len, GFP_KERNEL); ++ tsk->bound_addr_len = addr_len; ++ } ++ memcpy(tsk->bound_addr, addr, addr_len); ++ +release: + release_sock(sk); + return err; @@ -367,13 +415,12 @@ index 000000000000..f43a17cff3a3 + } + + tpc.svm_port = tsk->svm_port = vm_addr.svm_port; ++ tpc.family = tsk->family; + tpc.type = type; + + pr_debug("%s: type=%d\n", __func__, tpc.type); + -+ err = tsi_control_sendmsg(tsk->csocket, -+ TSI_PROXY_CREATE, -+ (void *)&tpc, ++ err = tsi_control_sendmsg(tsk->csocket, TSI_PROXY_CREATE, (void *)&tpc, + sizeof(struct tsi_proxy_create)); + if (err < 0) { + pr_debug("%s: error sending proxy request\n", __func__); @@ -383,21 +430,15 @@ index 000000000000..f43a17cff3a3 + return 0; +} + -+static int tsi_connect(struct socket *sock, struct sockaddr *addr, -+ int addr_len, int flags) ++static int tsi_connect(struct socket *sock, struct sockaddr *addr, int addr_len, ++ int flags) +{ -+ DECLARE_SOCKADDR(struct sockaddr_in *, sin, addr); + struct sock *sk = sock->sk; + struct tsi_sock *tsk; + struct socket *isocket; + struct socket *vsocket; + int err; + -+ if (sin->sin_family != AF_INET) { -+ pr_debug("%s: rejecting unknown family\n", __func__); -+ return -EINVAL; -+ } -+ + lock_sock(sk); + tsk = tsi_sk(sock->sk); + isocket = tsk->isocket; @@ -405,15 +446,28 @@ index 000000000000..f43a17cff3a3 + + pr_debug("%s: vsocket=%p isocket=%p\n", __func__, vsocket, isocket); + ++ if (addr->sa_family != tsk->family) { ++ pr_debug( ++ "%s: rejecting mismatched family addr->sa_family=%d tsk->family=%d\n", ++ __func__, addr->sa_family, tsk->family); ++ err = -EINVAL; ++ goto release; ++ } ++ ++ err = tsi_check_addr_len(tsk, addr_len); ++ if (err != 0) { ++ pr_debug("%s: invalid addr_len: %d\n", __func__, addr_len); ++ err = -EINVAL; ++ goto release; ++ } ++ + if (isocket) { + /* We can't honor O_NONBLOCK semantics here as we need to know + * whether this request can be fulfilled from INET or we need + * to fall back to VSOCK. + */ -+ err = -+ isocket->ops->connect(isocket, addr, addr_len, -+ flags & ~O_NONBLOCK); -+ pr_debug("%s: returned=%d\n", __func__, err); ++ err = isocket->ops->connect(isocket, addr, addr_len, ++ flags & ~O_NONBLOCK); + if (err == 0 || err == -EALREADY) { + tsk->status = S_INET; + pr_debug("%s: switching to CONNECTED_INET\n", __func__); @@ -440,14 +494,13 @@ index 000000000000..f43a17cff3a3 + } + + tc_req.svm_port = tsk->svm_port; -+ tc_req.addr = sin->sin_addr.s_addr; -+ tc_req.port = sin->sin_port; ++ tc_req.addr_len = addr_len; ++ memcpy(&tc_req.addr, addr, addr_len); + + pr_debug("%s: sending connection request id=%u\n", __func__, + tc_req.svm_port); + -+ err = tsi_control_sendmsg(tsk->csocket, -+ TSI_CONNECT, ++ err = tsi_control_sendmsg(tsk->csocket, TSI_CONNECT, + (void *)&tc_req, + sizeof(struct tsi_connect_req)); + if (err < 0) { @@ -456,14 +509,13 @@ index 000000000000..f43a17cff3a3 + goto release; + } + -+ err = tsi_control_recvmsg(tsk->csocket, -+ TSI_CONNECT, ++ err = tsi_control_recvmsg(tsk->csocket, TSI_CONNECT, + (void *)&tc_rsp, + sizeof(struct tsi_connect_rsp)); + if (err < 0) { -+ pr_debug -+ ("%s: error receiving connection request answer\n", -+ __func__); ++ pr_debug( ++ "%s: error receiving connection request answer\n", ++ __func__); + goto release; + } + @@ -491,10 +543,10 @@ index 000000000000..f43a17cff3a3 + } + + if (!tsk->sendto_addr) { -+ tsk->sendto_addr = -+ kmalloc(sizeof(struct sockaddr_in), GFP_KERNEL); ++ tsk->sendto_addr = kmalloc(addr_len, GFP_KERNEL); ++ tsk->sendto_addr_len = addr_len; + } -+ memcpy(tsk->sendto_addr, addr, sizeof(struct sockaddr_in)); ++ memcpy(tsk->sendto_addr, addr, addr_len); + + tsk->status = S_VSOCK; + } @@ -504,6 +556,40 @@ index 000000000000..f43a17cff3a3 + return err; +} + ++static int tsi_socketpair(struct socket *socka, struct socket *sockb) ++{ ++ struct sock *ska = socka->sk; ++ struct sock *skb = sockb->sk; ++ struct tsi_sock *tska; ++ struct tsi_sock *tskb; ++ struct socket *isocketa; ++ struct socket *isocketb; ++ int err; ++ ++ lock_sock(ska); ++ lock_sock(skb); ++ tska = tsi_sk(ska); ++ tskb = tsi_sk(skb); ++ isocketa = tska->isocket; ++ isocketb = tskb->isocket; ++ ++ if (tska->family != PF_UNIX || tskb->family != PF_UNIX) { ++ err = -EOPNOTSUPP; ++ goto release; ++ } ++ ++ err = isocketa->ops->socketpair(isocketa, isocketb); ++ if (err == 0) { ++ pr_debug("%s: switching to S_INET\n", __func__); ++ tska->status = tskb->status = S_INET; ++ } ++ ++release: ++ release_sock(ska); ++ release_sock(skb); ++ return err; ++} ++ +static int tsi_accept_inet(struct tsi_sock *tsk, struct socket **newsock, + struct proto_accept_arg *arg) +{ @@ -546,18 +632,14 @@ index 000000000000..f43a17cff3a3 + pr_debug("%s: sending accept request id=%u\n", __func__, + ta_req.svm_port); + -+ err = tsi_control_sendmsg(tsk->csocket, -+ TSI_ACCEPT, -+ (void *)&ta_req, ++ err = tsi_control_sendmsg(tsk->csocket, TSI_ACCEPT, (void *)&ta_req, + sizeof(struct tsi_accept_req)); + if (err < 0) { + pr_debug("%s: error sending accept request\n", __func__); + return err; + } + -+ err = tsi_control_recvmsg(tsk->csocket, -+ TSI_ACCEPT, -+ (void *)&ta_rsp, ++ err = tsi_control_recvmsg(tsk->csocket, TSI_ACCEPT, (void *)&ta_rsp, + sizeof(struct tsi_accept_rsp)); + if (err < 0) { + pr_debug("%s: error receiving accept response\n", __func__); @@ -610,15 +692,29 @@ index 000000000000..f43a17cff3a3 + pr_debug("%s: socket=%p newsock=%p st=%d\n", __func__, sock, newsock, + tsk->status); + -+ sk = sk_alloc(current->nsproxy->net_ns, AF_TSI, GFP_KERNEL, -+ &tsi_proto, 0); ++ if (tsk->family == PF_INET) { ++ sk = sk_alloc(current->nsproxy->net_ns, AF_TSI, GFP_KERNEL, ++ &tsi_in4_proto, 0); ++ } else if (tsk->family == PF_INET6) { ++ sk = sk_alloc(current->nsproxy->net_ns, AF_TSI6, GFP_KERNEL, ++ &tsi_in6_proto, 0); ++ } else if (tsk->family == PF_UNIX) { ++ sk = sk_alloc(current->nsproxy->net_ns, AF_TSIU, GFP_KERNEL, ++ &tsi_un_proto, 0); ++ } else { ++ panic("Unsupported family in tsk->family=%d", tsk->family); ++ } + if (!sk) { + err = -ENOMEM; + goto release; + } + + sock_init_data(newsock, sk); ++ /* We need to use our own setsockopt functions */ ++ set_bit(SOCK_CUSTOM_SOCKOPT, &sk->sk_socket->flags); ++ + newtsk = tsi_sk(newsock->sk); ++ newtsk->family = tsk->family; + + if (tsk->status == S_INET) { + err = tsi_accept_inet(tsk, &nsock, arg); @@ -633,15 +729,15 @@ index 000000000000..f43a17cff3a3 + goto error; + } + -+ err = -+ nsock->ops->getname(nsock, (struct sockaddr *)&vm_addr, 0); ++ err = nsock->ops->getname(nsock, (struct sockaddr *)&vm_addr, ++ 0); + if (err < 0) { + pr_debug("%s: error in getname: %d\n", __func__, err); + goto error; + } + newtsk->svm_port = vm_addr.svm_port; -+ err = -+ nsock->ops->getname(nsock, (struct sockaddr *)&vm_addr, 1); ++ err = nsock->ops->getname(nsock, (struct sockaddr *)&vm_addr, ++ 1); + if (err < 0) { + pr_debug("%s: error in peer getname: %d\n", __func__, + err); @@ -667,50 +763,50 @@ index 000000000000..f43a17cff3a3 + +error: + if (nsock) -+ nsock->ops->release(nsock); ++ sock_release(nsock); + release_sock(listener); + return err; +} + -+static int vsock_proxy_getname(struct tsi_sock *tsk, -+ struct sockaddr *addr, int peer) ++static int vsock_proxy_getname(struct tsi_sock *tsk, struct sockaddr *addr, ++ int peer) +{ + struct tsi_getname_req gn_req; + struct tsi_getname_rsp gn_rsp; -+ int addr_len; + int err; -+ DECLARE_SOCKADDR(struct sockaddr_in *, sin, addr); + + gn_req.svm_port = tsk->svm_port; + gn_req.svm_peer_port = tsk->svm_peer_port; + gn_req.peer = peer; + -+ err = tsi_control_sendmsg(tsk->csocket, -+ TSI_GETNAME, -+ (void *)&gn_req, ++ err = tsi_control_sendmsg(tsk->csocket, TSI_GETNAME, (void *)&gn_req, + sizeof(struct tsi_getname_req)); + if (err < 0) { + pr_debug("%s: error sending getname request\n", __func__); + return err; + } + -+ err = tsi_control_recvmsg(tsk->csocket, -+ TSI_GETNAME, -+ (void *)&gn_rsp, ++ err = tsi_control_recvmsg(tsk->csocket, TSI_GETNAME, (void *)&gn_rsp, + sizeof(struct tsi_getname_rsp)); + if (err < 0) { + pr_debug("%s: error receiving getname answer\n", __func__); + return err; + } + -+ sin->sin_family = AF_INET; -+ sin->sin_port = gn_rsp.port; -+ sin->sin_addr.s_addr = gn_rsp.addr; -+ addr_len = sizeof(struct sockaddr_in); ++ if (gn_rsp.result != 0) { ++ return gn_rsp.result; ++ } ++ ++ err = tsi_check_addr_len(tsk, gn_rsp.addr_len); ++ if (err != 0) { ++ pr_debug("%s: invalid addr_len: %d\n", __func__, ++ gn_rsp.addr_len); ++ return -EINVAL; ++ } + -+ memcpy(addr, sin, addr_len); ++ memcpy(addr, gn_rsp.addr, gn_rsp.addr_len); + -+ return addr_len; ++ return gn_rsp.addr_len; +} + +static int tsi_getname(struct socket *sock, struct sockaddr *addr, int peer) @@ -719,6 +815,8 @@ index 000000000000..f43a17cff3a3 + struct tsi_sock *tsk; + struct socket *isocket; + DECLARE_SOCKADDR(struct sockaddr_in *, sin, addr); ++ DECLARE_SOCKADDR(struct sockaddr_in6 *, sin6, addr); ++ DECLARE_SOCKADDR(struct sockaddr_un *, sun, addr); + int err; + + lock_sock(sk); @@ -739,11 +837,27 @@ index 000000000000..f43a17cff3a3 + } else if (isocket) { + err = isocket->ops->getname(isocket, addr, peer); + } else { -+ sin->sin_family = AF_INET; -+ sin->sin_port = htons(1234); -+ sin->sin_addr.s_addr = htonl(2130706433); -+ memset(sin->sin_zero, 0, sizeof(sin->sin_zero)); -+ err = sizeof(*sin); ++ if (tsk->family == PF_INET) { ++ sin->sin_family = AF_INET; ++ sin->sin_port = htons(1234); ++ sin->sin_addr.s_addr = htonl(2130706433); ++ memset(sin->sin_zero, 0, sizeof(sin->sin_zero)); ++ err = sizeof(*sin); ++ } else if (tsk->family == PF_INET6) { ++ sin6->sin6_family = AF_INET6; ++ sin6->sin6_port = htons(1234); ++ sin6->sin6_flowinfo = 0; ++ memset(sin6->sin6_addr.s6_addr, 0, sizeof(sin6->sin6_addr.s6_addr)); ++ sin6->sin6_addr.s6_addr[15] = 1; ++ sin6->sin6_scope_id = 0; ++ err = sizeof(*sin6); ++ } else if (tsk->family == PF_UNIX) { ++ sun->sun_family = AF_UNIX; ++ sun->sun_path[0] = 0; ++ err = 2; ++ } else { ++ err = -EPROTONOSUPPORT; ++ } + } + break; + } @@ -753,7 +867,7 @@ index 000000000000..f43a17cff3a3 +} + +static __poll_t tsi_poll(struct file *file, struct socket *sock, -+ poll_table * wait) ++ poll_table *wait) +{ + struct sock *sk = sock->sk; + struct tsi_sock *tsk; @@ -766,8 +880,8 @@ index 000000000000..f43a17cff3a3 + isocket = tsk->isocket; + vsocket = tsk->vsocket; + -+ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, -+ vsocket, isocket, tsk->status); ++ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, vsocket, ++ isocket, tsk->status); + + switch (tsk->status) { + case S_INET: @@ -794,11 +908,13 @@ index 000000000000..f43a17cff3a3 + struct sock *sk = sock->sk; + struct tsi_sock *tsk; + struct socket *isocket; ++ struct socket *vsocket; + int err; + + lock_sock(sk); + tsk = tsi_sk(sock->sk); + isocket = tsk->isocket; ++ vsocket = tsk->vsocket; + + switch (tsk->status) { + case S_HYBRID: @@ -810,7 +926,11 @@ index 000000000000..f43a17cff3a3 + } + break; + case S_VSOCK: -+ err = -EOPNOTSUPP; ++ if (vsocket) { ++ err = vsocket->ops->ioctl(vsocket, cmd, arg); ++ } else { ++ err = -EOPNOTSUPP; ++ } + break; + } + @@ -825,7 +945,6 @@ index 000000000000..f43a17cff3a3 + struct socket *isocket; + struct socket *vsocket; + struct sockaddr_vm vm_addr; -+ struct sockaddr_in *sin; + struct tsi_listen_req lreq; + struct tsi_listen_rsp lrsp; + int err; @@ -854,7 +973,6 @@ index 000000000000..f43a17cff3a3 + err = -EINVAL; + goto release; + } -+ sin = tsk->bound_addr; + + if (!tsk->svm_port) { + if (tsi_create_proxy(tsk, SOCK_STREAM) != 0) { @@ -864,25 +982,21 @@ index 000000000000..f43a17cff3a3 + } + + lreq.svm_port = tsk->svm_port; -+ lreq.addr = sin->sin_addr.s_addr; -+ lreq.port = sin->sin_port; ++ lreq.addr_len = tsk->bound_addr_len; ++ memcpy(&lreq.addr, tsk->bound_addr, tsk->bound_addr_len); + lreq.vm_port = vm_addr.svm_port; + lreq.backlog = backlog; + -+ pr_debug("%s: requesting to listen on port=%d\n", __func__, lreq.port); -+ -+ err = tsi_control_sendmsg(tsk->csocket, -+ TSI_LISTEN, -+ (void *)&lreq, sizeof(struct tsi_listen_req)); ++ err = tsi_control_sendmsg(tsk->csocket, TSI_LISTEN, (void *)&lreq, ++ sizeof(struct tsi_listen_req)); + if (err < 0) { + pr_debug("%s: error sending listen request: %d\n", __func__, + err); + goto release; + } + -+ err = tsi_control_recvmsg(tsk->csocket, -+ TSI_LISTEN, -+ (void *)&lrsp, sizeof(struct tsi_listen_rsp)); ++ err = tsi_control_recvmsg(tsk->csocket, TSI_LISTEN, (void *)&lrsp, ++ sizeof(struct tsi_listen_rsp)); + if (err < 0) { + pr_debug("%s: error receiving listen request answer\n", + __func__); @@ -895,7 +1009,7 @@ index 000000000000..f43a17cff3a3 + + if (err == 0) { + tsk->svm_peer_port = TSI_DEFAULT_PORT; -+ } else if (err == -EPERM) { ++ } else if (err == -EPERM || err == -EADDRINUSE || err == -EADDRNOTAVAIL) { + /* Our peer is telling us we're not allowed to expose this + * port. Switch to the INET personality. + */ @@ -926,8 +1040,8 @@ index 000000000000..f43a17cff3a3 + isocket = tsk->isocket; + vsocket = tsk->vsocket; + -+ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, -+ vsocket, isocket, tsk->status); ++ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, vsocket, ++ isocket, tsk->status); + + switch (tsk->status) { + case S_HYBRID: @@ -945,9 +1059,7 @@ index 000000000000..f43a17cff3a3 + return err; +} + -+static int tsi_stream_setsockopt(struct socket *sock, -+ int level, -+ int optname, ++static int tsi_stream_setsockopt(struct socket *sock, int level, int optname, + sockptr_t optval, unsigned int optlen) +{ + struct sock *sk = sock->sk; @@ -959,14 +1071,21 @@ index 000000000000..f43a17cff3a3 + tsk = tsi_sk(sock->sk); + isocket = tsk->isocket; + -+ pr_debug("%s: s=%p is=%p st=%d\n", __func__, sock, -+ isocket, tsk->status); ++ pr_debug("%s: s=%p is=%p st=%d family=%d\n", __func__, sock, isocket, ++ tsk->status, tsk->family); + + switch (tsk->status) { + case S_HYBRID: + case S_INET: -+ err = isocket->ops->setsockopt(isocket, level, optname, optval, -+ optlen); ++ if (level == SOL_SOCKET) { ++ err = sock_setsockopt(isocket, level, optname, optval, ++ optlen); ++ } else if (isocket->ops->setsockopt) { ++ err = isocket->ops->setsockopt(isocket, level, optname, ++ optval, optlen); ++ } else { ++ err = -ENOPROTOOPT; ++ } + break; + case S_VSOCK: + // TODO implement remote setsockopt @@ -978,9 +1097,7 @@ index 000000000000..f43a17cff3a3 + return err; +} + -+static int tsi_dgram_setsockopt(struct socket *sock, -+ int level, -+ int optname, ++static int tsi_dgram_setsockopt(struct socket *sock, int level, int optname, + sockptr_t optval, unsigned int optlen) +{ + struct sock *sk = sock->sk; @@ -992,14 +1109,21 @@ index 000000000000..f43a17cff3a3 + tsk = tsi_sk(sock->sk); + isocket = tsk->isocket; + -+ pr_debug("%s: s=%p is=%p st=%d\n", __func__, sock, -+ isocket, tsk->status); ++ pr_debug("%s: s=%p is=%p st=%d\n", __func__, sock, isocket, ++ tsk->status); + + switch (tsk->status) { + case S_HYBRID: + case S_INET: -+ err = isocket->ops->setsockopt(isocket, level, optname, optval, -+ optlen); ++ if (level == SOL_SOCKET) { ++ err = sock_setsockopt(isocket, level, optname, optval, ++ optlen); ++ } else if (isocket->ops->setsockopt) { ++ err = isocket->ops->setsockopt(isocket, level, optname, ++ optval, optlen); ++ } else { ++ err = -ENOPROTOOPT; ++ } + break; + case S_VSOCK: + // TODO implement remote setsockopt @@ -1011,9 +1135,8 @@ index 000000000000..f43a17cff3a3 + return err; +} + -+static int tsi_stream_getsockopt(struct socket *sock, -+ int level, int optname, -+ char *optval, int __user * optlen) ++static int tsi_stream_getsockopt(struct socket *sock, int level, int optname, ++ char *optval, int __user *optlen) +{ + struct sock *sk = sock->sk; + struct tsi_sock *tsk; @@ -1026,8 +1149,8 @@ index 000000000000..f43a17cff3a3 + isocket = tsk->isocket; + vsocket = tsk->vsocket; + -+ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, -+ vsocket, isocket, tsk->status); ++ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, vsocket, ++ isocket, tsk->status); + + switch (tsk->status) { + case S_HYBRID: @@ -1060,8 +1183,8 @@ index 000000000000..f43a17cff3a3 + isocket = tsk->isocket; + vsocket = tsk->vsocket; + -+ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, -+ vsocket, isocket, tsk->status); ++ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, vsocket, ++ isocket, tsk->status); + + switch (tsk->status) { + case S_HYBRID: @@ -1081,12 +1204,95 @@ index 000000000000..f43a17cff3a3 + return err; +} + ++/* ++ * Check if there's a UDP listener for the given destination in the guest. ++ * Returns true if there's a UDP socket bound to the destination address:port. ++ * This works for any address (IPv4 or IPv6), including: ++ * - Localhost (127.0.0.1, ::1) ++ * - Regular local addresses ++ * - Custom-routed "remote" addresses (e.g., if 8.8.8.8 is routed locally) ++ */ ++static bool tsi_has_udp_listener(struct sock *sk, struct sockaddr *addr, ++ int addr_len) ++{ ++ struct tsi_sock *tsk; ++ struct net *net; ++ struct sock *listener; ++ ++ tsk = tsi_sk(sk); ++ ++ if (WARN_ON_ONCE(sk->sk_type != SOCK_DGRAM)) ++ return false; ++ ++ if (!addr) ++ return false; ++ ++ net = sock_net(sk); ++ ++ if (tsk->family == PF_INET) { ++ struct sockaddr_in *sin; ++ __be32 daddr; ++ __be16 dport; ++ ++ if (addr_len < sizeof(struct sockaddr_in)) ++ return false; ++ ++ sin = (struct sockaddr_in *)addr; ++ if (sin->sin_family != AF_INET) ++ return false; ++ ++ daddr = sin->sin_addr.s_addr; ++ dport = sin->sin_port; ++ ++ /* Check if anyone is listening on that destination address:port */ ++ listener = __udp4_lib_lookup(net, ++ 0, 0, /* source addr/port - don't care */ ++ daddr, dport, /* dest addr/port */ ++ 0, 0, /* device indices */ ++ net->ipv4.udp_table, ++ NULL); ++ ++ if (listener) { ++ sock_put(listener); /* Release the reference */ ++ return true; ++ } ++ } else if (tsk->family == PF_INET6) { ++ struct sockaddr_in6 *sin6; ++ const struct in6_addr *daddr; ++ __be16 dport; ++ ++ if (addr_len < sizeof(struct sockaddr_in6)) ++ return false; ++ ++ sin6 = (struct sockaddr_in6 *)addr; ++ if (sin6->sin6_family != AF_INET6) ++ return false; ++ ++ daddr = &sin6->sin6_addr; ++ dport = sin6->sin6_port; ++ ++ /* Check if anyone is listening on that destination address:port */ ++ listener = __udp6_lib_lookup(net, ++ NULL, 0, /* source addr/port - don't care */ ++ daddr, dport, /* dest addr/port */ ++ 0, 0, /* device indices */ ++ net->ipv4.udp_table, ++ NULL); ++ ++ if (listener) { ++ sock_put(listener); /* Release the reference */ ++ return true; ++ } ++ } ++ ++ return false; ++} ++ +static int tsi_dgram_sendmsg(struct socket *sock, struct msghdr *msg, + size_t len) +{ + struct sock *sk = sock->sk; + struct tsi_sendto_addr sa_req; -+ struct sockaddr_in *sin; + struct sockaddr_vm *svm; + struct tsi_sock *tsk; + struct socket *isocket; @@ -1098,29 +1304,59 @@ index 000000000000..f43a17cff3a3 + isocket = tsk->isocket; + vsocket = tsk->vsocket; + -+ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, -+ vsocket, isocket, tsk->status); ++ pr_debug("%s: s=%p vs=%p is=%p st=%d family=%d\n", __func__, sock, ++ vsocket, isocket, tsk->status, tsk->family); + + switch (tsk->status) { + case S_INET: + err = isocket->ops->sendmsg(isocket, msg, len); + break; -+ case S_HYBRID: -+ err = isocket->ops->sendmsg(isocket, msg, len); -+ if (err == 0) -+ goto release; ++ case S_HYBRID: { ++ bool has_local_listener; ++ ++ /* ++ * For datagram sockets, we need to decide whether to send via ++ * the guest's inet stack or proxy via vsock to the host. ++ * ++ * Strategy: Check if there's a listener in the guest. If yes, ++ * send via inet (guest priority). If no, proxy via vsock to ++ * reach host. ++ */ ++ has_local_listener = tsi_has_udp_listener(sk, msg->msg_name, ++ msg->msg_namelen); ++ if (has_local_listener) { ++ err = isocket->ops->sendmsg(isocket, msg, len); ++ if (err >= 0) ++ goto release; ++ /* ++ * Genuine error (negative). Iterator might have been ++ * consumed if this is a late error (Type B). ++ * Fall through to vsock as last resort. ++ */ ++ iov_iter_revert(&msg->msg_iter, len); ++ } ++ /* ++ * No local listener OR inet failed - proxy via vsock to host. ++ * This is the common case when guest apps try to reach host ++ * services. ++ */ + fallthrough; ++ } + case S_VSOCK: + if (msg->msg_name) { + pr_debug("%s: fixing msg_name for vsock proxy\n", + __func__); ++ err = tsi_check_addr_len(tsk, msg->msg_namelen); ++ if (err != 0) ++ goto release; ++ + if (!tsk->sendto_addr) { + tsk->sendto_addr = -+ kmalloc(sizeof(struct sockaddr_in), -+ GFP_KERNEL); ++ kmalloc(msg->msg_namelen, GFP_KERNEL); ++ tsk->sendto_addr_len = msg->msg_namelen; + } + memcpy(tsk->sendto_addr, msg->msg_name, -+ sizeof(struct sockaddr_in)); ++ msg->msg_namelen); + + if (tsk->svm_port == 0) { + if (tsi_create_proxy(tsk, SOCK_DGRAM) != 0) { @@ -1129,20 +1365,17 @@ index 000000000000..f43a17cff3a3 + } + } + -+ sin = (struct sockaddr_in *)msg->msg_name; + sa_req.svm_port = tsk->svm_port; -+ sa_req.addr = sin->sin_addr.s_addr; -+ sa_req.port = sin->sin_port; -+ -+ err = tsi_control_sendmsg(tsk->csocket, -+ TSI_SENDTO_ADDR, -+ (void *)&sa_req, -+ sizeof(struct -+ tsi_sendto_addr)); ++ sa_req.addr_len = msg->msg_namelen; ++ memcpy(&sa_req.addr, msg->msg_name, msg->msg_namelen); ++ ++ err = tsi_control_sendmsg( ++ tsk->csocket, TSI_SENDTO_ADDR, (void *)&sa_req, ++ sizeof(struct tsi_sendto_addr)); + if (err < 0) { -+ pr_debug -+ ("%s: error sending connection request: %d\n", -+ __func__, err); ++ pr_debug( ++ "%s: error sending connection request: %d\n", ++ __func__, err); + goto release; + } + @@ -1154,9 +1387,8 @@ index 000000000000..f43a17cff3a3 + } + + err = vsocket->ops->sendmsg(vsocket, msg, len); -+ if (err == 0) { ++ if (err > 0) + tsk->status = S_VSOCK; -+ } + break; + } + @@ -1179,8 +1411,8 @@ index 000000000000..f43a17cff3a3 + isocket = tsk->isocket; + vsocket = tsk->vsocket; + -+ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, -+ vsocket, isocket, tsk->status); ++ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, vsocket, ++ isocket, tsk->status); + + switch (tsk->status) { + case S_HYBRID: @@ -1212,25 +1444,24 @@ index 000000000000..f43a17cff3a3 + isocket = tsk->isocket; + vsocket = tsk->vsocket; + -+ pr_debug("%s: s=%p vs=%p is=%p st=%d\n", __func__, sock, -+ vsocket, isocket, tsk->status); ++ pr_debug("%s: s=%p vs=%p is=%p st=%d family=%d\n", __func__, sock, ++ vsocket, isocket, tsk->status, tsk->family); + + switch (tsk->status) { + case S_HYBRID: -+ err = -ENOTCONN; -+ break; + case S_INET: + err = isocket->ops->recvmsg(isocket, msg, len, flags); + break; + case S_VSOCK: + err = vsocket->ops->recvmsg(vsocket, msg, len, flags); + if (err > 0 && msg && msg->msg_name && tsk->sendto_addr) { -+ pr_debug -+ ("%s: msg_name=%p sin_sendto=%p, msg_len=%d sin_len=%ld\n", -+ __func__, msg->msg_name, tsk->sendto_addr, -+ msg->msg_namelen, sizeof(struct sockaddr_in)); ++ pr_debug( ++ "%s: msg_name=%p sendto_addr=%p, msg_len=%d addr_len=%d\n", ++ __func__, msg->msg_name, tsk->sendto_addr, ++ msg->msg_namelen, tsk->sendto_addr_len); + memcpy(msg->msg_name, tsk->sendto_addr, -+ sizeof(struct sockaddr_in)); ++ tsk->sendto_addr_len); ++ msg->msg_namelen = tsk->sendto_addr_len; + } + break; + } @@ -1245,7 +1476,7 @@ index 000000000000..f43a17cff3a3 + .release = tsi_release, + .bind = tsi_bind, + .connect = tsi_connect, -+ .socketpair = sock_no_socketpair, ++ .socketpair = tsi_socketpair, + .accept = tsi_accept, + .getname = tsi_getname, + .poll = tsi_poll, @@ -1265,7 +1496,7 @@ index 000000000000..f43a17cff3a3 + .release = tsi_release, + .bind = tsi_bind, + .connect = tsi_connect, -+ .socketpair = sock_no_socketpair, ++ .socketpair = tsi_socketpair, + .accept = tsi_accept, + .getname = tsi_getname, + .poll = tsi_poll, @@ -1279,8 +1510,8 @@ index 000000000000..f43a17cff3a3 + .mmap = sock_no_mmap, +}; + -+static int tsi_create(struct net *net, struct socket *sock, -+ int protocol, int kern) ++static int tsi_create_common(struct net *net, struct socket *sock, int protocol, ++ int kern, int family) +{ + struct tsi_sock *tsk; + struct socket *isocket; @@ -1289,7 +1520,7 @@ index 000000000000..f43a17cff3a3 + struct sock *sk; + int err; + -+ pr_debug("%s: socket=%p\n", __func__, sock); ++ pr_debug("%s: socket=%p family=%d\n", __func__, sock, family); + + if (!sock) + return -EINVAL; @@ -1305,37 +1536,47 @@ index 000000000000..f43a17cff3a3 + return -ESOCKTNOSUPPORT; + } + -+ sk = sk_alloc(net, AF_TSI, GFP_KERNEL, &tsi_proto, kern); ++ if (family == PF_INET) { ++ sk = sk_alloc(net, AF_TSI, GFP_KERNEL, &tsi_in4_proto, kern); ++ } else if (family == PF_INET6) { ++ sk = sk_alloc(net, AF_TSI6, GFP_KERNEL, &tsi_in6_proto, kern); ++ } else if (family == PF_UNIX) { ++ sk = sk_alloc(net, AF_TSIU, GFP_KERNEL, &tsi_un_proto, kern); ++ } else { ++ return -EPROTONOSUPPORT; ++ } + if (!sk) + return -ENOMEM; + + sock_init_data(sock, sk); ++ /* We need to use our own setsockopt functions */ ++ set_bit(SOCK_CUSTOM_SOCKOPT, &sk->sk_socket->flags); + + tsk = tsi_sk(sk); + + isocket = NULL; -+ err = __sock_create(current->nsproxy->net_ns, PF_INET, -+ sock->type, protocol, &isocket, 1); ++ err = __sock_create(current->nsproxy->net_ns, family, sock->type, ++ protocol, &isocket, 1); + if (err) { -+ pr_err("%s (%d): problem creating inet socket\n", -+ __func__, task_pid_nr(current)); -+ goto release_isocket; ++ pr_debug("%s (%d): problem creating inet socket\n", __func__, ++ task_pid_nr(current)); ++ return err; + } + + vsocket = NULL; -+ err = __sock_create(current->nsproxy->net_ns, PF_VSOCK, -+ sock->type, PF_VSOCK, &vsocket, 1); ++ err = __sock_create(current->nsproxy->net_ns, PF_VSOCK, sock->type, ++ PF_VSOCK, &vsocket, 1); + if (err) { -+ pr_err("%s (%d): problem creating vsock socket\n", -+ __func__, task_pid_nr(current)); -+ goto release_vsocket; ++ pr_debug("%s (%d): problem creating vsock socket\n", __func__, ++ task_pid_nr(current)); ++ goto free_isocket; + } + + err = tsi_create_control_socket(&csocket); + if (err) { -+ pr_err("%s (%d): problem creating control socket\n", -+ __func__, task_pid_nr(current)); -+ goto release_vsocket; ++ pr_debug("%s (%d): problem creating control socket\n", __func__, ++ task_pid_nr(current)); ++ goto free_vsocket; + } + + pr_debug("isocket: %p\n", isocket); @@ -1348,19 +1589,50 @@ index 000000000000..f43a17cff3a3 + tsk->svm_peer_port = TSI_DEFAULT_PORT; + tsk->sendto_addr = NULL; + tsk->bound_addr = NULL; ++ tsk->family = family; + + return 0; + -+release_vsocket: -+ vsocket->ops->release(vsocket); -+release_isocket: -+ isocket->ops->release(isocket); ++free_vsocket: ++ sock_release(vsocket); ++free_isocket: ++ sock_release(isocket); + return err; +} + -+static const struct net_proto_family tsi_family_ops = { ++static int tsi_create_in4(struct net *net, struct socket *sock, int protocol, ++ int kern) ++{ ++ return tsi_create_common(net, sock, protocol, kern, AF_INET); ++} ++ ++static int tsi_create_in6(struct net *net, struct socket *sock, int protocol, ++ int kern) ++{ ++ return tsi_create_common(net, sock, protocol, kern, AF_INET6); ++} ++ ++static int tsi_create_un(struct net *net, struct socket *sock, int protocol, ++ int kern) ++{ ++ return tsi_create_common(net, sock, protocol, kern, AF_UNIX); ++} ++ ++static const struct net_proto_family tsi_family_in4_ops = { + .family = AF_TSI, -+ .create = tsi_create, ++ .create = tsi_create_in4, ++ .owner = THIS_MODULE, ++}; ++ ++static const struct net_proto_family tsi_family_in6_ops = { ++ .family = AF_TSI6, ++ .create = tsi_create_in6, ++ .owner = THIS_MODULE, ++}; ++ ++static const struct net_proto_family tsi_family_un_ops = { ++ .family = AF_TSIU, ++ .create = tsi_create_un, + .owner = THIS_MODULE, +}; + @@ -1368,24 +1640,56 @@ index 000000000000..f43a17cff3a3 +{ + int err; + -+ tsi_proto.owner = THIS_MODULE; -+ -+ err = proto_register(&tsi_proto, 1); ++ err = proto_register(&tsi_in4_proto, 1); + if (err) { + pr_err("Could not register tsi protocol\n"); + goto err_do_nothing; + } -+ err = sock_register(&tsi_family_ops); ++ err = sock_register(&tsi_family_in4_ops); + if (err) { -+ pr_err("could not register af_tsi (%d) address family: %d\n", ++ pr_err("Could not register af_tsi (%d) address family: %d\n", + AF_TSI, err); -+ goto err_unregister_proto; ++ goto err_unregister_in4_proto; ++ } ++ ++ err = proto_register(&tsi_in6_proto, 1); ++ if (err) { ++ pr_err("Could not register tsi6 protocol\n"); ++ goto err_unregister_in4_sock; ++ } ++ ++ err = sock_register(&tsi_family_in6_ops); ++ if (err) { ++ pr_err("Could not register af_tsi6 (%d) address family: %d\n", ++ AF_TSI6, err); ++ goto err_unregister_in6_proto; ++ } ++ ++ err = proto_register(&tsi_un_proto, 1); ++ if (err) { ++ pr_debug("Could not register tsiu protocol\n"); ++ goto err_unregister_in6_sock; ++ } ++ ++ err = sock_register(&tsi_family_un_ops); ++ if (err) { ++ pr_debug("Could not register af_tsiu (%d) address family: %d\n", ++ AF_TSIU, err); ++ goto err_unregister_un_proto; + } + + return 0; + -+err_unregister_proto: -+ proto_unregister(&tsi_proto); ++err_unregister_un_proto: ++ proto_unregister(&tsi_un_proto); ++err_unregister_in6_sock: ++ sock_unregister(AF_TSI6); ++err_unregister_in6_proto: ++ proto_unregister(&tsi_in6_proto); ++err_unregister_in4_sock: ++ sock_unregister(AF_TSI); ++err_unregister_in4_proto: ++ proto_unregister(&tsi_in4_proto); +err_do_nothing: + return err; +} @@ -1393,7 +1697,9 @@ index 000000000000..f43a17cff3a3 +static void __exit tsi_exit(void) +{ + sock_unregister(AF_TSI); -+ proto_unregister(&tsi_proto); ++ proto_unregister(&tsi_in4_proto); ++ sock_unregister(AF_TSI6); ++ proto_unregister(&tsi_in6_proto); +} + +module_init(tsi_init); @@ -1405,10 +1711,10 @@ index 000000000000..f43a17cff3a3 +MODULE_LICENSE("GPL v2"); diff --git a/net/tsi/af_tsi.h b/net/tsi/af_tsi.h new file mode 100644 -index 000000000000..cf381734bebe +index 000000000000..b74966fd7ae3 --- /dev/null +++ b/net/tsi/af_tsi.h -@@ -0,0 +1,100 @@ +@@ -0,0 +1,107 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Transparent Socket Impersonation Driver @@ -1437,15 +1743,18 @@ index 000000000000..cf381734bebe +#define TSI_ACCEPT 1030 +#define TSI_PROXY_RELEASE 1031 + ++#define TSI_ADDR_LEN 128 ++ +struct tsi_proxy_create { + u32 svm_port; ++ u16 family; + u16 type; +} __attribute__((packed)); + +struct tsi_connect_req { + u32 svm_port; -+ u32 addr; -+ u16 port; ++ u32 addr_len; ++ char addr[TSI_ADDR_LEN]; +} __attribute__((packed)); + +struct tsi_connect_rsp { @@ -1454,16 +1763,16 @@ index 000000000000..cf381734bebe + +struct tsi_sendto_addr { + u32 svm_port; -+ u32 addr; -+ u16 port; ++ u32 addr_len; ++ char addr[TSI_ADDR_LEN]; +} __attribute__((packed)); + +struct tsi_listen_req { + u32 svm_port; -+ u32 addr; -+ u16 port; + u32 vm_port; -+ int backlog; ++ u32 backlog; ++ u32 addr_len; ++ char addr[TSI_ADDR_LEN]; +} __attribute__((packed)); + +struct tsi_listen_rsp { @@ -1486,8 +1795,9 @@ index 000000000000..cf381734bebe +} __attribute__((packed)); + +struct tsi_getname_rsp { -+ u32 addr; -+ u16 port; ++ int result; ++ u32 addr_len; ++ char addr[TSI_ADDR_LEN]; +} __attribute__((packed)); + +struct tsi_sock { @@ -1499,8 +1809,11 @@ index 000000000000..cf381734bebe + unsigned int status; + u32 svm_port; + u32 svm_peer_port; -+ struct sockaddr_in *bound_addr; -+ struct sockaddr_in *sendto_addr; ++ struct sockaddr *bound_addr; ++ struct sockaddr *sendto_addr; ++ int bound_addr_len; ++ int sendto_addr_len; ++ u16 family; +}; + +struct tsi_proxy_release { @@ -1510,22 +1823,26 @@ index 000000000000..cf381734bebe + +#endif diff --git a/security/selinux/hooks.c b/security/selinux/hooks.c -index fc926d3cac6e..486be0734a6c 100644 +index 8e31d3b60fc6..aae3267fa9ee 100644 --- a/security/selinux/hooks.c +++ b/security/selinux/hooks.c -@@ -1302,7 +1302,9 @@ static inline u16 socket_type_to_security_class(int family, int type, int protoc +@@ -1302,7 +1302,13 @@ static inline u16 socket_type_to_security_class(int family, int type, int protoc return SECCLASS_XDP_SOCKET; case PF_MCTP: return SECCLASS_MCTP_SOCKET; -#if PF_MAX > 46 + case PF_TSI: + return SECCLASS_TSI_SOCKET; -+#if PF_MAX > 47 ++ case PF_TSI6: ++ return SECCLASS_TSI_SOCKET; ++ case PF_TSIU: ++ return SECCLASS_TSI_SOCKET; ++#if PF_MAX > 49 #error New address family defined, please update this function. #endif } diff --git a/security/selinux/include/classmap.h b/security/selinux/include/classmap.h -index 7229c9bf6c27..065d9b85693f 100644 +index 7229c9bf6c27..f08d24321603 100644 --- a/security/selinux/include/classmap.h +++ b/security/selinux/include/classmap.h @@ -173,6 +173,7 @@ const struct security_class_mapping secclass_map[] = { @@ -1541,9 +1858,9 @@ index 7229c9bf6c27..065d9b85693f 100644 }; -#if PF_MAX > 46 -+#if PF_MAX > 47 ++#if PF_MAX > 49 #error New address family defined, please update secclass_map. #endif -- -2.50.0 +2.54.0 diff --git a/kernel/patches/0010-tsi-allow-hijacking-sockets-tsi_hijack.patch b/kernel/patches/0012-tsi-allow-hijacking-sockets-tsi_hijack.patch similarity index 52% rename from kernel/patches/0010-tsi-allow-hijacking-sockets-tsi_hijack.patch rename to kernel/patches/0012-tsi-allow-hijacking-sockets-tsi_hijack.patch index 8e633861..6da309bf 100644 --- a/kernel/patches/0010-tsi-allow-hijacking-sockets-tsi_hijack.patch +++ b/kernel/patches/0012-tsi-allow-hijacking-sockets-tsi_hijack.patch @@ -1,54 +1,66 @@ -From 0935e6b153ffeecfbe5509971805ad2bf5b53bb0 Mon Sep 17 00:00:00 2001 +From 74fbbeb0dcc7e3eb2281b2f10f5ddb3aaa658287 Mon Sep 17 00:00:00 2001 From: Sergio Lopez Date: Thu, 19 May 2022 22:42:01 +0200 -Subject: [PATCH 10/21] tsi: allow hijacking sockets (tsi_hijack) +Subject: [PATCH 12/30] tsi: allow hijacking sockets (tsi_hijack) Add a kernel command line option (tsi_hijack) enabling users to request the kernel to hijack AF_INET(SOCK_STREAM || SOCK_DGRAM) -sockets to turn them into TSI sockets. +sockets to turn them into TSI sockets, and another one +(tsi_hijack_unix) to do the same thing for AF_UNIX sockets. Signed-off-by: Sergio Lopez --- - net/socket.c | 17 +++++++++++++++++ + net/socket.c | 28 ++++++++++++++++++++++++++++ net/tsi/af_tsi.c | 2 +- - 2 files changed, 18 insertions(+), 1 deletion(-) + 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/net/socket.c b/net/socket.c -index 5ad75d15e1ad..bf8818423454 100644 +index a55927134851..a355cbd6ba1e 100644 --- a/net/socket.c +++ b/net/socket.c -@@ -115,6 +115,10 @@ unsigned int sysctl_net_busy_read __read_mostly; +@@ -115,6 +115,11 @@ unsigned int sysctl_net_busy_read __read_mostly; unsigned int sysctl_net_busy_poll __read_mostly; #endif +#ifdef CONFIG_TSI +bool tsi_hijack = false; ++bool tsi_hijack_unix = false; +#endif + static ssize_t sock_read_iter(struct kiocb *iocb, struct iov_iter *to); static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from); static int sock_mmap(struct file *file, struct vm_area_struct *vma); -@@ -1488,6 +1492,10 @@ int sock_wake_async(struct socket_wq *wq, int how, int band) +@@ -1471,6 +1476,11 @@ int sock_wake_async(struct socket_wq *wq, int how, int band) } EXPORT_SYMBOL(sock_wake_async); +#ifdef CONFIG_TSI +core_param(tsi_hijack, tsi_hijack, bool, 0644); ++core_param(tsi_hijack_unix, tsi_hijack_unix, bool, 0644); +#endif + /** * __sock_create - creates a socket * @net: net namespace -@@ -1558,6 +1566,15 @@ int __sock_create(struct net *net, int family, int type, int protocol, +@@ -1541,6 +1551,24 @@ int __sock_create(struct net *net, int family, int type, int protocol, request_module("net-pf-%d", family); #endif +#ifdef CONFIG_TSI -+ if (tsi_hijack && !kern && family == AF_INET && -+ (type == SOCK_STREAM || type == SOCK_DGRAM)) { -+ pr_debug("%s - tsi: hijacking AF_INET socket\n", -+ current->comm); -+ family = AF_TSI; ++ if (!kern && (type == SOCK_STREAM || type == SOCK_DGRAM)) { ++ if (family == AF_INET && tsi_hijack) { ++ pr_debug("%s - tsi: hijacking AF_INET socket\n", ++ current->comm); ++ family = AF_TSI; ++ } else if (family == AF_INET6 && tsi_hijack) { ++ pr_debug("%s - tsi: hijacking AF_INET6 socket\n", ++ current->comm); ++ family = AF_TSI6; ++ } else if (family == AF_UNIX && tsi_hijack_unix) { ++ pr_debug("%s - tsi: hijacking AF_UNIX socket\n", ++ current->comm); ++ family = AF_TSIU; ++ } + } +#endif + @@ -56,10 +68,10 @@ index 5ad75d15e1ad..bf8818423454 100644 pf = rcu_dereference(net_families[family]); err = -EAFNOSUPPORT; diff --git a/net/tsi/af_tsi.c b/net/tsi/af_tsi.c -index f43a17cff3a3..ef1552862253 100644 +index 3d2bcd8d2ba4..bd3dc75b3129 100644 --- a/net/tsi/af_tsi.c +++ b/net/tsi/af_tsi.c -@@ -474,7 +474,7 @@ static int tsi_accept(struct socket *sock, struct socket *newsock, +@@ -547,7 +547,7 @@ static int tsi_accept(struct socket *sock, struct socket *newsock, struct socket *csocket; struct tsi_sock *tsk; struct tsi_sock *newtsk; @@ -69,5 +81,5 @@ index f43a17cff3a3..ef1552862253 100644 int err; -- -2.50.0 +2.54.0 From b7e50c5e4e28198fca8f6cc4d41a9e5dc8336d44 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Thu, 9 Jul 2026 13:29:10 -0700 Subject: [PATCH 4/5] shim: don't demote already-real-root shim into a new user namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cloneMntNs unconditionally added CLONE_NEWUSER even when the shim process already had real root (euid 0). Entering a *new* user namespace — even one mapping a UID to itself — demotes the process to a non-initial user namespace, and the kernel restricts mounting real block-device-backed filesystems (e.g. ext4) to the initial user namespace regardless of the effective capabilities held within a descendant namespace. When the shim already has real root, skip CLONE_NEWUSER and use CLONE_NEWNS alone: this still provides the mount-namespace isolation and cleanup-on-exit benefit without losing the ability to mount real filesystems. Found and fixed while working on the multi worktree's sandbox rootfs assembly path (which does need real ext4/loop mounts and hit this as "operation not permitted" when run as real root), but the bug applies here identically: this file only differs from multi's in UID-mapping style, not in the underlying logic. Verified: full conformance suite passes as both root and non-root (44/44 both ways). Signed-off-by: Derek McGowan --- pkg/shim/manager/mount_linux.go | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/pkg/shim/manager/mount_linux.go b/pkg/shim/manager/mount_linux.go index d19b9b12..18e685f7 100644 --- a/pkg/shim/manager/mount_linux.go +++ b/pkg/shim/manager/mount_linux.go @@ -25,12 +25,14 @@ import ( "syscall" ) -// cloneMntNs configures the child command to start in a new user + mount -// namespace. The user namespace provides mount isolation and grants the -// child capabilities within it, without requiring or granting real host -// capabilities. User namespaces are available unprivileged on many -// distros (since Linux 3.8), but some may gate them via sysctl (e.g. -// kernel.apparmor_restrict_unprivileged_userns on Ubuntu). +// cloneMntNs configures the child command to start in a new mount +// namespace, and — only when the calling process is not already real +// root — a new user namespace as well. The user namespace provides mount +// isolation and grants the child capabilities within it, without +// requiring or granting real host capabilities. User namespaces are +// available unprivileged on many distros (since Linux 3.8), but some may +// gate them via sysctl (e.g. kernel.apparmor_restrict_unprivileged_userns +// on Ubuntu). // // For a VM-based runtime like nerdbox, the shim does not need real host // root — it needs /dev/kvm access (checked against mapped host UID) and @@ -51,11 +53,29 @@ import ( // container delete, and the VM itself performs all container-visible // filesystem setup. // +// When the calling process already has real root (euid 0), we deliberately +// skip CLONE_NEWUSER: entering a *new* user namespace — even one that maps +// a UID to itself — demotes the process to a non-initial user namespace, +// and the kernel restricts mounting real block-device-backed filesystems +// (e.g. ext4) to the initial user namespace regardless of the effective +// capabilities held within a descendant namespace. Real root gets +// CLONE_NEWNS alone, which still provides the mount-namespace +// isolation/cleanup-on-exit benefit without losing the ability to mount +// real filesystems. +// // If namespace creation is not possible (e.g. AppArmor restricts // unprivileged user namespaces), the shim runs without mount isolation // and this function returns false. // cloneMntNs returns true if user namespace clone flags were set. func cloneMntNs(_ context.Context, cmd *exec.Cmd) bool { + if os.Geteuid() == 0 { + // Already real root: a plain mount namespace is enough, and + // avoids demoting into a non-initial user namespace (which would + // break mounts of real block-device filesystems). + cmd.SysProcAttr.Cloneflags |= syscall.CLONE_NEWNS + return false + } + if restricted, err := apparmorRestrictsUserns(); err != nil { // Failed to check apparmor userns restriction, skipping mount namespace isolation") // We can't log anything here as it will break the TTRPC protocol! From 87befa5f61205b1856db79d18517d8af324fa573 Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Fri, 10 Jul 2026 17:05:21 -0700 Subject: [PATCH 5/5] shim: don't require config.json in the sandbox bundle at shim Start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit manager.Start (the shim-v2 "start" hook, invoked when containerd spawns the shim binary) unconditionally called readSpec(), which os.Open()s "config.json" in the current directory (the bundle dir) and fails hard if it's missing. This is only used to look up an optional grouping annotation (io.containerd.runc.v2.group / io.kubernetes.cri.sandbox-id) for shim socket grouping — the fallback (grouping = the instance ID) is already set before the lookup runs. This broke real CRI usage of the shim sandboxer entirely: containerd's shim sandboxer (plugins/sandbox/controller.go, controllerLocal.Create) calls core/runtime/v2.NewBundle(ctx, root, state, id, info.Spec), which only writes config.json when info.Spec is non-nil — and CRI's RunPodSandbox (internal/cri/server/sandbox_run.go) never sets sandboxInfo.Spec before calling CreateSandbox; the real OCI spec only arrives later, over the Bundle TTRPC service, once the sandbox is already running. So for every CRI-created sandbox, config.json simply does not exist yet when Start runs, and every RunPodSandbox failed with: "io.containerd.nerdbox.v1: open config.json: no such file or directory: exit status 1" Fix: tolerate os.IsNotExist from readSpec() in Start (both the unix and windows implementations) by falling back to an empty spec, so grouping stays at its default (the instance ID). Any other read error (bad permissions, corrupt JSON) is still fatal, matching the existing TestReadSpec/TestReadSpecMissing unit tests, which already assert readSpec() itself returns os.IsNotExist for a missing file — the bug was only in the caller not tolerating that specific, expected error. Found while wiring the shim through a real containerd + CRI + critest harness (test/critest/ in the multi worktree) configured with a RuntimeClass-style runtime handler using the shim sandboxer (sandboxer = "shim", not "podsandbox"). Verified: a full RunPodSandbox -> CreateContainer -> StartContainer -> ExecSync -> Stop/Remove cycle now succeeds against a real containerd v2.3.2 CRI plugin. Signed-off-by: Derek McGowan --- pkg/shim/manager/manager_unix.go | 19 ++++++++++++++++--- pkg/shim/manager/manager_windows.go | 13 ++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/pkg/shim/manager/manager_unix.go b/pkg/shim/manager/manager_unix.go index 089872b9..81b3301d 100644 --- a/pkg/shim/manager/manager_unix.go +++ b/pkg/shim/manager/manager_unix.go @@ -142,12 +142,25 @@ func (manager) Start(ctx context.Context, bparams *bootapi.BootstrapParams) (_ * return nil, err } grouping := id - spec, err := readSpec() + sp, err := readSpec() if err != nil { - return nil, err + // The sandbox bundle has no config.json when containerd's shim + // sandboxer creates a sandbox via CRI: core/runtime/v2.NewBundle + // only writes config.json when the sandbox.Sandbox passed to + // CreateSandbox carries a non-nil Spec, which CRI's RunPodSandbox + // never sets before this Start hook runs (the real OCI spec only + // arrives later, over the Bundle TTRPC service). Grouping by + // annotation is an optional convenience (falls back to the + // instance ID, already the default above), not something Start + // should fail over — only a genuine read error (not "missing + // file") is fatal. + if !os.IsNotExist(err) { + return nil, err + } + sp = &spec{} } for _, group := range groupLabels { - if groupID, ok := spec.Annotations[group]; ok { + if groupID, ok := sp.Annotations[group]; ok { grouping = groupID break } diff --git a/pkg/shim/manager/manager_windows.go b/pkg/shim/manager/manager_windows.go index 5662a4a4..80a69f03 100644 --- a/pkg/shim/manager/manager_windows.go +++ b/pkg/shim/manager/manager_windows.go @@ -93,12 +93,19 @@ func (manager) Start(ctx context.Context, bparams *bootapi.BootstrapParams) (_ * return nil, err } grouping := id - spec, err := readSpec() + sp, err := readSpec() if err != nil { - return nil, err + // See the identical comment in manager_unix.go's Start: the sandbox + // bundle has no config.json when containerd's shim sandboxer + // creates a sandbox via CRI, and grouping-by-annotation is an + // optional convenience, not something Start should fail over. + if !os.IsNotExist(err) { + return nil, err + } + sp = &spec{} } for _, group := range groupLabels { - if groupID, ok := spec.Annotations[group]; ok { + if groupID, ok := sp.Annotations[group]; ok { grouping = groupID break }