diff --git a/docs/package/index.md b/docs/package/index.md index 29435aeb1..0cfb6f650 100644 --- a/docs/package/index.md +++ b/docs/package/index.md @@ -70,6 +70,9 @@ Except of the above, `urunc` accepts the following optional annotations: - `com.urunc.unikernel.mountRootfs`: A boolean value that if it is `true`, requests from `urunc` to mount the container's image rootfs in the unikernel (either as a block device or through shared-fs). +- `com.urunc.unikernel.advertiseDNS`: A boolean value that if it is `true`, + passes the DNS server obtained from the container's `/etc/resolv.conf` to + the guest. If it is absent or `false`, no DNS server is passed to the guest. 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 @@ -186,6 +189,7 @@ LABEL "com.urunc.unikernel.binary"=/unikernel/kernel LABEL "com.urunc.unikernel.initrd"=/unikernel/initrd LABEL "com.urunc.unikernel.unikernelType"="unikraft" LABEL "com.urunc.unikernel.hypervisor"="qemu" +LABEL "com.urunc.unikernel.advertiseDNS"="true" CMD ["nginx", "-c", "/nginx/conf/nginx.conf"] ``` diff --git a/pkg/unikontainers/annotations.go b/pkg/unikontainers/annotations.go index 6b577b872..f82d3b634 100644 --- a/pkg/unikontainers/annotations.go +++ b/pkg/unikontainers/annotations.go @@ -51,6 +51,7 @@ const ( annotMountRootfs = "com.urunc.unikernel.mountRootfs" annotNetDev = "com.urunc.unikernel.solo5NetDev" annotBlkDev = "com.urunc.unikernel.solo5BlkDev" + annotAdvertiseDNS = "com.urunc.unikernel.advertiseDNS" annotVAccel = "com.urunc.unikernel.vAccel" annotRPCAddress = "com.urunc.unikernel.RPCAddress" ) @@ -103,6 +104,7 @@ type UnikernelConfig struct { MountRootfs string `json:"com.urunc.unikernel.mountRootfs"` NetDev string `json:"com.urunc.unikernel.solo5NetDev,omitempty"` BlkDev string `json:"com.urunc.unikernel.solo5BlkDev,omitempty"` + AdvertiseDNS string `json:"com.urunc.unikernel.advertiseDNS,omitempty"` // The vAccel annotations are deliberately not part of urunc.json, since their // values are runtime specific and therefore we should only reach them // through the annotations of the spec. @@ -188,6 +190,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { MountRootfs := spec.Annotations[annotMountRootfs] netDev := spec.Annotations[annotNetDev] blkDev := spec.Annotations[annotBlkDev] + advertiseDNS := spec.Annotations[annotAdvertiseDNS] vAccel := spec.Annotations[annotVAccel] rpcAddress := spec.Annotations[annotRPCAddress] uniklog.WithFields(logrus.Fields{ @@ -201,6 +204,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { "mountRootfs": MountRootfs, "netDev": netDev, "blkDev": blkDev, + "advertiseDNS": advertiseDNS, "vAccel": vAccel, "rpcAddress": rpcAddress, }).WithField("source", "spec").Debug("urunc annotations") @@ -216,6 +220,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { MountRootfs: MountRootfs, NetDev: netDev, BlkDev: blkDev, + AdvertiseDNS: advertiseDNS, VAccel: vAccel, RPCAddress: rpcAddress, } @@ -258,6 +263,7 @@ func getConfigFromJSON(jsonFilePath string) (*UnikernelConfig, error) { "mountRootfs": tryDecode(conf.MountRootfs), "netDev": tryDecode(conf.NetDev), "blkDev": tryDecode(conf.BlkDev), + "advertiseDNS": tryDecode(conf.AdvertiseDNS), }).WithField("source", uruncJSONFilename).Debug("urunc annotations") return &conf, nil @@ -333,6 +339,12 @@ func (c *UnikernelConfig) decode() error { } c.BlkDev = string(decoded) + decoded, err = base64.StdEncoding.DecodeString(c.AdvertiseDNS) + if err != nil { + return fmt.Errorf("failed to decode advertiseDNS: %v", err) + } + c.AdvertiseDNS = string(decoded) + return nil } @@ -369,6 +381,9 @@ func (c *UnikernelConfig) Map() map[string]string { if c.BlkDev != "" { myMap[annotBlkDev] = c.BlkDev } + if c.AdvertiseDNS != "" { + myMap[annotAdvertiseDNS] = c.AdvertiseDNS + } if c.VAccel != "" { myMap[annotVAccel] = c.VAccel } @@ -423,6 +438,13 @@ func (c *UnikernelConfig) validateValues() error { } } + if c.AdvertiseDNS != "" { + _, err = strconv.ParseBool(c.AdvertiseDNS) + if err != nil { + return fmt.Errorf("invalid value %q for %s: expected a boolean: %w", c.AdvertiseDNS, annotAdvertiseDNS, err) + } + } + err = validateSolo5DevName(annotNetDev, c.NetDev) if err != nil { return err diff --git a/pkg/unikontainers/annotations_test.go b/pkg/unikontainers/annotations_test.go index 39f2e4b63..5c68c2044 100644 --- a/pkg/unikontainers/annotations_test.go +++ b/pkg/unikontainers/annotations_test.go @@ -42,6 +42,7 @@ func TestGetConfigFromSpec(t *testing.T) { annotMountRootfs: "true", annotNetDev: "management", annotBlkDev: "database", + annotAdvertiseDNS: "true", }, } @@ -55,6 +56,7 @@ func TestGetConfigFromSpec(t *testing.T) { MountRootfs: "true", NetDev: "management", BlkDev: "database", + AdvertiseDNS: "true", } config := getConfigFromSpec(spec) @@ -240,6 +242,7 @@ func TestMap(t *testing.T) { MountRootfs: "false", NetDev: "netdev_value", BlkDev: "blkdev_value", + AdvertiseDNS: "true", VAccel: "vsock", RPCAddress: "vsock://2:1234", } @@ -253,6 +256,7 @@ func TestMap(t *testing.T) { annotMountRootfs: "false", annotNetDev: "netdev_value", annotBlkDev: "blkdev_value", + annotAdvertiseDNS: "true", annotVAccel: "vsock", annotRPCAddress: "vsock://2:1234", } @@ -346,6 +350,7 @@ func TestValidateValues(t *testing.T) { {"solo5 device", annotNetDev, "management"}, {"empty solo5 device", annotNetDev, ""}, {"empty mountRootfs", annotMountRootfs, ""}, + {"advertise DNS enabled", annotAdvertiseDNS, "true"}, } for _, tc := range accepted { @@ -362,6 +367,7 @@ func TestValidateValues(t *testing.T) { val string }{ {"non boolean mountRootfs", annotMountRootfs, "yes"}, + {"non boolean advertise DNS", annotAdvertiseDNS, "yes"}, {"current directory binary", annotBinary, "."}, {"current directory mountpoint", annotBlockMntPoint, "."}, {"root binary", annotBinary, "/"}, diff --git a/pkg/unikontainers/monitor_spec_test.go b/pkg/unikontainers/monitor_spec_test.go index 423261f7f..9f99d3b0b 100644 --- a/pkg/unikontainers/monitor_spec_test.go +++ b/pkg/unikontainers/monitor_spec_test.go @@ -103,6 +103,36 @@ func TestWriteMonitorSpec(t *testing.T) { assert.Equal(t, "/", got.GuestParams.Rootfs.MonRootfs) }) + t.Run("filters the DNS server based on the advertise DNS annotation", func(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + advertiseDNS string + expectedDNS string + }{ + {name: "enabled", advertiseDNS: "true", expectedDNS: "1.1.1.1"}, + {name: "disabled", advertiseDNS: "false", expectedDNS: ""}, + {name: "absent", expectedDNS: ""}, + } { + t.Run(tc.name, func(t *testing.T) { + monRootfs := t.TempDir() + u, rootfsParams := newSpecUnikontainer(t, monRootfs) + if tc.advertiseDNS != "" { + u.State.Annotations[annotAdvertiseDNS] = tc.advertiseDNS + } + resolvConf := filepath.Join(t.TempDir(), "resolv.conf") + require.NoError(t, os.WriteFile(resolvConf, []byte("nameserver 1.1.1.1\n"), 0o600)) + u.Spec.Mounts = []specs.Mount{{Destination: "/etc/resolv.conf", Source: resolvConf}} + + err := u.writeMonitorSpec(rootfsParams, monitorResources{}) + require.NoError(t, err) + + got := readMonitorSpecFile(t, monRootfs) + assert.Equal(t, tc.expectedDNS, got.DNSServer) + }) + } + }) + t.Run("does not persist the monitor environment", func(t *testing.T) { // t.Setenv forbids t.Parallel. t.Setenv("URUNC_TEST_SECRET", "do-not-write-me") diff --git a/pkg/unikontainers/unikernels/hermit_rs.go b/pkg/unikontainers/unikernels/hermit_rs.go index 3a587f640..41cff8212 100644 --- a/pkg/unikontainers/unikernels/hermit_rs.go +++ b/pkg/unikontainers/unikernels/hermit_rs.go @@ -31,9 +31,10 @@ type Hermit struct { } type HermitNet struct { - Address string - Mask int - Gateway string + Address string + Mask int + Gateway string + DNSServer string } func (h *Hermit) CommandString() (string, error) { @@ -45,6 +46,9 @@ func (h *Hermit) CommandString() (string, error) { if h.Net.Gateway != "" { args = append(args, fmt.Sprintf("gateway=%s", h.Net.Gateway)) } + if h.Net.DNSServer != "" { + args = append(args, fmt.Sprintf("env=HERMIT_DNS1=%s", h.Net.DNSServer)) + } // Add separator ONLY if we have net args AND a command appArgs := strings.TrimSpace(h.Command) @@ -119,6 +123,7 @@ func (h *Hermit) Init(data types.UnikernelParams) error { h.Net.Address = data.Net.IP h.Net.Gateway = data.Net.Gateway h.Net.Mask = mask + h.Net.DNSServer = data.Net.DNSServer } h.Command = strings.Join(data.CmdLine, " ") diff --git a/pkg/unikontainers/unikernels/hermit_rs_test.go b/pkg/unikontainers/unikernels/hermit_rs_test.go new file mode 100644 index 000000000..a1ac6cfc5 --- /dev/null +++ b/pkg/unikontainers/unikernels/hermit_rs_test.go @@ -0,0 +1,56 @@ +package unikernels + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHermitCommandString(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + hermit *Hermit + expected string + }{ + { + name: "no network configured", + hermit: &Hermit{}, + expected: "", + }, + { + name: "with network configured", + hermit: &Hermit{ + Net: HermitNet{ + Address: "10.0.0.2", + Mask: 24, + Gateway: "10.0.0.1", + }, + }, + expected: "ip=10.0.0.2/24 gateway=10.0.0.1", + }, + { + name: "with DNS configured", + hermit: &Hermit{ + Net: HermitNet{ + Address: "10.0.0.2", + Mask: 24, + Gateway: "10.0.0.1", + DNSServer: "1.1.1.1", + }, + }, + expected: "ip=10.0.0.2/24 gateway=10.0.0.1 env=HERMIT_DNS1=1.1.1.1", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + result, err := tc.hermit.CommandString() + require.NoError(t, err) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/pkg/unikontainers/unikernels/mewz.go b/pkg/unikontainers/unikernels/mewz.go index 808e4c22c..75ca95f17 100644 --- a/pkg/unikontainers/unikernels/mewz.go +++ b/pkg/unikontainers/unikernels/mewz.go @@ -30,14 +30,21 @@ type Mewz struct { } type MewzNet struct { - Address string - Mask int - Gateway string + Address string + Mask int + Gateway string + DNSServer string } func (m *Mewz) CommandString() (string, error) { if m.Net.Address != "" { - return fmt.Sprintf("ip=%s/%d gateway=%s", m.Net.Address, m.Net.Mask, m.Net.Gateway), nil + args := fmt.Sprintf("ip=%s/%d gateway=%s", m.Net.Address, m.Net.Mask, m.Net.Gateway) + + if m.Net.DNSServer != "" { + args += fmt.Sprintf(" dns=%s", m.Net.DNSServer) + } + + return args, nil } return "", nil } @@ -100,6 +107,7 @@ func (m *Mewz) Init(data types.UnikernelParams) error { m.Net.Address = data.Net.IP m.Net.Gateway = data.Net.Gateway m.Net.Mask = mask + m.Net.DNSServer = data.Net.DNSServer return nil } diff --git a/pkg/unikontainers/unikernels/mewz_test.go b/pkg/unikontainers/unikernels/mewz_test.go index c4ac12a07..d50ae88c4 100644 --- a/pkg/unikontainers/unikernels/mewz_test.go +++ b/pkg/unikontainers/unikernels/mewz_test.go @@ -45,6 +45,18 @@ func TestMewzCommandString(t *testing.T) { }, expected: "ip=10.0.0.2/24 gateway=10.0.0.1", }, + { + name: "with DNS configured", + mewz: &Mewz{ + Net: MewzNet{ + Address: "10.0.0.2", + Mask: 24, + Gateway: "10.0.0.1", + DNSServer: "1.1.1.1", + }, + }, + expected: "ip=10.0.0.2/24 gateway=10.0.0.1 dns=1.1.1.1", + }, } for _, tc := range testCases { diff --git a/pkg/unikontainers/unikernels/mirage.go b/pkg/unikontainers/unikernels/mirage.go index ecd73c8bd..023ca5a49 100644 --- a/pkg/unikontainers/unikernels/mirage.go +++ b/pkg/unikontainers/unikernels/mirage.go @@ -33,8 +33,9 @@ type Mirage struct { } type MirageNet struct { - Address string - Gateway string + Address string + Gateway string + DNSServer string } type MirageBlock struct { @@ -43,9 +44,12 @@ type MirageBlock struct { } func (m *Mirage) CommandString() (string, error) { - return fmt.Sprintf("%s %s %s", m.Net.Address, - m.Net.Gateway, - m.Command), nil + command := fmt.Sprintf("%s %s", m.Net.Address, m.Net.Gateway) + if m.Net.DNSServer != "" { + command += fmt.Sprintf(" --dns-servers=udp:%s,tcp:%s", m.Net.DNSServer, m.Net.DNSServer) + } + + return command + " " + m.Command, nil } func (m *Mirage) SupportsBlock() bool { @@ -123,6 +127,7 @@ func (m *Mirage) Init(data types.UnikernelParams) error { m.Net.Address = fmt.Sprintf("--ipv4=%s/%d", data.Net.IP, mask) m.Net.Gateway = "--ipv4-gateway=" + data.Net.Gateway } + m.Net.DNSServer = data.Net.DNSServer } m.Block = make([]MirageBlock, 0, len(data.Block)) for _, blk := range data.Block { diff --git a/pkg/unikontainers/unikernels/mirage_test.go b/pkg/unikontainers/unikernels/mirage_test.go index 7a01b8a23..e5c36395c 100644 --- a/pkg/unikontainers/unikernels/mirage_test.go +++ b/pkg/unikontainers/unikernels/mirage_test.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/urunc-dev/urunc/pkg/unikontainers/types" ) @@ -125,3 +126,41 @@ func TestMirageBlkDevName(t *testing.T) { assert.Equal(t, "storage", args[0].ID) }) } + +func TestMirageDNS(t *testing.T) { + testCases := []struct { + name string + dnsServer string + expected string + }{ + { + name: "DNS server present", + dnsServer: "1.1.1.1", + expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 --dns-servers=udp:1.1.1.1,tcp:1.1.1.1 app", + }, + { + name: "DNS server absent", + expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 app", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := newMirage() + err := m.Init(types.UnikernelParams{ + CmdLine: []string{"app"}, + Net: types.NetDevParams{ + IP: "10.0.0.2", + Mask: "255.255.255.0", + Gateway: "10.0.0.1", + DNSServer: tc.dnsServer, + }, + }) + require.NoError(t, err) + + cmd, err := m.CommandString() + require.NoError(t, err) + assert.Equal(t, tc.expected, cmd) + }) + } +} diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index 8c101ed19..21ac4df6d 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -25,6 +25,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "sync" "syscall" @@ -554,8 +555,10 @@ func (u *Unikontainer) buildMonitorSpec(rootfsParams types.RootfsParams, monRes mSpec.PreStartCmd = monRes.PreStartCmd // Resolve the guest DNS server once, here in the builder shared by both the // libcontainer and non-libcontainer paths, where the container mount - // sources are available. - mSpec.DNSServer = getDNSServer(u.Spec.Mounts) + // sources are available. Only advertise it when explicitly enabled. + if advertiseDNS, _ := strconv.ParseBool(u.State.Annotations[annotAdvertiseDNS]); advertiseDNS { + mSpec.DNSServer = getDNSServer(u.Spec.Mounts) + } return mSpec }