Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/contributors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,6 @@ users:
Inuth0603:
name: Divyam Agarwal
email: iitdivyamagarwal@gmail.com
indrajithek:
name: Indrajith
email: 29561593+indrajithek@users.noreply.github.com
69 changes: 69 additions & 0 deletions cmd/urunc/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2023-2026, Nubificus LTD
//
// 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 main

import (
"context"
"errors"
"os"

"github.com/sirupsen/logrus"
"github.com/urfave/cli/v3"
)

var errExecNotSupported = errors.New("exec is not supported for urunc guests")

// execCommand handles exec for containers that urunc handed over to runc.
// The flags mirror the ones of runc exec, so that the arguments containerd
// passes are accepted. They are not used by urunc, since runc gets the
// original arguments.
var execCommand = &cli.Command{
Name: "exec",
Usage: "execute new process inside a container handed over to runc",
ArgsUsage: `<container-id> <command> [command options] || -p process.json <container-id>`,
Flags: []cli.Flag{
&cli.StringFlag{Name: "console-socket"},
&cli.StringFlag{Name: "pidfd-socket"},
&cli.StringFlag{Name: "cwd"},
&cli.StringSliceFlag{Name: "env", Aliases: []string{"e"}},
&cli.BoolFlag{Name: "tty", Aliases: []string{"t"}},
&cli.StringFlag{Name: "user", Aliases: []string{"u"}},
&cli.StringSliceFlag{Name: "additional-gids", Aliases: []string{"g"}},
&cli.StringFlag{Name: "process", Aliases: []string{"p"}},
&cli.BoolFlag{Name: "detach", Aliases: []string{"d"}},
&cli.StringFlag{Name: "pid-file"},
&cli.StringFlag{Name: "process-label"},
&cli.StringFlag{Name: "apparmor"},
&cli.BoolFlag{Name: "no-new-privs"},
&cli.StringSliceFlag{Name: "cap", Aliases: []string{"c"}},
&cli.IntFlag{Name: "preserve-fds"},
&cli.StringSliceFlag{Name: "cgroup"},
&cli.BoolFlag{Name: "ignore-paused"},
},
Action: func(_ context.Context, cmd *cli.Command) error {
logrus.WithField("command", "EXEC").WithField("args", os.Args).Debug("urunc INVOKED")
if err := checkArgs(cmd, 1, minArgs); err != nil {
return err
}

// For a container that urunc handed over to runc, this execs runc
// with the original arguments and does not return.
if _, err := getUnikontainer(cmd); err != nil {
return err
}

return errExecNotSupported
},
}
1 change: 1 addition & 0 deletions cmd/urunc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func main() {
Commands: []*cli.Command{
createCommand,
deleteCommand,
execCommand,
killCommand,
monitorCommand,
runCommand,
Expand Down
5 changes: 5 additions & 0 deletions docs/package/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ Apart from the above, `urunc` accepts the following optional annotations:
guest may serve on, as a comma-separated list of ports and `LOW-HIGH` ranges
(e.g. `8080,9000-9100`). Without it the guest serves on any port, as a
container does. Only supported by `hyperlight-unikraft`.
- `com.urunc.unikernel.init`: The absolute path to the init of a Linux guest
inside the container's rootfs (e.g. `/urunit`). The container's command then
runs under that init, which is useful when something else sets the command
(i.e., [Argo Workflows](https://argoproj.github.io/workflows/)). A value in
the annotations of the container replaces the one in `urunc.json`.

Due to the fact that [Docker](https://www.docker.com/) and some high-level
container runtimes do not pass the image annotations to the underlying container
Expand Down
9 changes: 7 additions & 2 deletions docs/tutorials/existing-container-linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,13 @@ designed specifically for `urunc`. It performs the following actions:
the container in a Kubernetes cluster, where there is a high chance that
the gateway of the container might be in a different subnet than the IP.
As a result, the Linux kernel will fail to set the gateway.
2. Groups multi-word arguments correctly.
2. Reads the application command and its arguments from the configuration.
3. Mounts `/proc` and `/sys`.
4. Reads and sets the environment variables for the application execution
5. Reads and sets the uid,gid and working directory for the application execution
6. Acts as a reaper, cleaning up zombie processes.
6. Gives the application `/dev/null` as stdin when the container has no stdin
attached, instead of the console of the guest.
7. Acts as a reaper, cleaning up zombie processes.

To pass the necessary information to
[urunit](https://github.com/nubificus/urunit), `urunc` uses a configuration
Expand All @@ -95,6 +97,9 @@ UCS
UID:<uid>
GID:<gid>
WD:<working_dir>
STDIN:0 /* only when the container has no stdin attached */
ARC:<number of arguments>
ARV:<argument> /* one line per argument */
UCE
```

Expand Down
4 changes: 2 additions & 2 deletions pkg/network/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,12 @@ type Interface struct {
MTU int
}

func NewNetworkManager(networkType string) (Manager, error) {
func NewNetworkManager(networkType string, linuxGuest bool) (Manager, error) {
switch networkType {
case "static":
return &StaticNetwork{}, nil
case "dynamic":
return &DynamicNetwork{}, nil
return &DynamicNetwork{linuxGuest: linuxGuest}, nil
default:
return nil, fmt.Errorf("network manager %s not supported", networkType)

Expand Down
17 changes: 17 additions & 0 deletions pkg/network/network_dynamic.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import (
)

type DynamicNetwork struct {
// linuxGuest is set when the guest is Linux, so its own connections use
// the local port range of the Linux kernel.
linuxGuest bool
}

// NetworkSetup checks if any tap device is available in the current netns. If it is, it assumes a running unikernel
Expand Down Expand Up @@ -56,6 +59,20 @@ func (n DynamicNetwork) NetworkSetup(uid uint32, gid uint32) (*UnikernelNetworkI
}
netlog.Debugf("tap device created: %s", newTapDevice.Attrs().Name)

// The redirect sends every packet of the pod to the guest. If the pod
// keeps a local port range for its own connections, the other containers
// of the pod still need the replies to them.
if n.linuxGuest {
if low, high, ok := podPortBand(); ok {
err = addPodPortFilters(redirectLink, newTapDevice, low, high)
if err != nil {
netlog.Warnf("failed to keep ports %d-%d for the pod, all traffic goes to the guest: %v", low, high, err)
} else {
netlog.Infof("keeping ARP and ports %d-%d of %s for the pod", low, high, redirectLink.Attrs().Name)
}
}
}

netlog.Debugf("fetching info for %s", redirectLink.Attrs().Name)
ifInfo, err := getInterfaceInfo(redirectLink.Attrs().Name)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/network/network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestNewNetworkManager(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := NewNetworkManager(tt.networkType)
got, err := NewNetworkManager(tt.networkType, false)
if tt.expectedErr {
assert.Error(t, err, "NewNetworkManager() should return an error")
} else {
Expand Down
141 changes: 141 additions & 0 deletions pkg/network/pod_ports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright (c) 2023-2026, Nubificus LTD
//
// 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 network

import (
"errors"
"fmt"
"os"
"strconv"
"strings"

"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
"golang.org/x/sys/unix"
)

const (
// podPortRangePath holds the local port range of the network namespace
// of the reading process, which is the one of the pod.
podPortRangePath = "/proc/sys/net/ipv4/ip_local_port_range"
// linuxGuestPortMax is the end of the default local port range of the
// Linux kernel (32768-60999), which a Linux guest uses for its own
// connections.
linuxGuestPortMax = 60999
)

// podPortBand reads the local port range of the pod's network namespace
// and reports whether it can be kept for the pod's own connections.
func podPortBand() (uint16, uint16, bool) {
data, err := os.ReadFile(podPortRangePath)
if err != nil {
netlog.Debugf("failed to read %s: %v", podPortRangePath, err)
return 0, 0, false
}
return parsePortBand(string(data))
}

// parsePortBand parses a local port range ("low high"). The range is usable
// only when it is entirely above the local port range of a Linux guest, so
// that the ports of the guest's connections and services stay with the guest.
func parsePortBand(s string) (uint16, uint16, bool) {
fields := strings.Fields(s)
if len(fields) != 2 {
return 0, 0, false
}
low, err := strconv.ParseUint(fields[0], 10, 16)
if err != nil {
return 0, 0, false
}
high, err := strconv.ParseUint(fields[1], 10, 16)
if err != nil {
return 0, 0, false
}
if low <= linuxGuestPortMax || low > high {
return 0, 0, false
}
return uint16(low), uint16(high), true
}

// addPodPortFilters adds filters on the ingress of eth that run before the
// redirect to the tap: ARP is copied to the tap and also passed to the pod,
// and TCP and UDP packets to the ports in [low, high] are passed to the pod.
// In that way, the other containers of the pod keep the replies to their own
// connections. If a filter can not be added, the ones already added are
// removed, leaving only the redirect.
func addPodPortFilters(eth netlink.Link, tap netlink.Link, low uint16, high uint16) error {
tcp := nl.IPPROTO_TCP
udp := nl.IPPROTO_UDP
filters := []netlink.Filter{
&netlink.Flower{
FilterAttrs: podFilterAttrs(eth, 1, unix.ETH_P_ARP),
EthType: unix.ETH_P_ARP,
Actions: []netlink.Action{
&netlink.MirredAction{
ActionAttrs: netlink.ActionAttrs{
Action: netlink.TC_ACT_OK,
},
MirredAction: netlink.TCA_EGRESS_MIRROR,
Ifindex: tap.Attrs().Index,
},
},
},
podPortFilter(eth, 2, &tcp, low, high),
podPortFilter(eth, 3, &udp, low, high),
}

var added []netlink.Filter
for _, filter := range filters {
err := netlink.FilterAdd(filter)
if err != nil {
var errs []error
for _, f := range added {
if delErr := netlink.FilterDel(f); delErr != nil {
errs = append(errs, delErr)
}
}
return errors.Join(fmt.Errorf("failed to add filter: %w", err), errors.Join(errs...))
}
added = append(added, filter)
}

return nil
}

func podFilterAttrs(link netlink.Link, priority uint16, protocol uint16) netlink.FilterAttrs {
return netlink.FilterAttrs{
LinkIndex: link.Attrs().Index,
Parent: netlink.MakeHandle(0xffff, 0),
Priority: priority,
Protocol: protocol,
}
}

func podPortFilter(link netlink.Link, priority uint16, proto *nl.IPProto, low uint16, high uint16) *netlink.Flower {
return &netlink.Flower{
FilterAttrs: podFilterAttrs(link, priority, unix.ETH_P_IP),
EthType: unix.ETH_P_IP,
IPProto: proto,
DstPortRangeMin: low,
DstPortRangeMax: high,
Actions: []netlink.Action{
&netlink.GenericAction{
ActionAttrs: netlink.ActionAttrs{
Action: netlink.TC_ACT_OK,
},
},
},
}
}
Loading