From b97c928a28db3b3b36ed630ce6a342b5fc71376c Mon Sep 17 00:00:00 2001 From: slash Date: Wed, 16 Sep 2026 16:43:51 +0530 Subject: [PATCH 1/5] feat: add DNS server to guest boot args Signed-off-by: slash --- pkg/unikontainers/unikernels/hermit_rs.go | 11 ++- .../unikernels/hermit_rs_test.go | 88 +++++++++++++++++++ pkg/unikontainers/unikernels/mewz.go | 16 +++- pkg/unikontainers/unikernels/mewz_test.go | 44 ++++++++++ pkg/unikontainers/unikernels/mirage.go | 13 ++- pkg/unikontainers/unikernels/mirage_test.go | 39 ++++++++ 6 files changed, 202 insertions(+), 9 deletions(-) create mode 100644 pkg/unikontainers/unikernels/hermit_rs_test.go 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..01ce0a9c7 --- /dev/null +++ b/pkg/unikontainers/unikernels/hermit_rs_test.go @@ -0,0 +1,88 @@ +package unikernels + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/urunc-dev/urunc/pkg/unikontainers/types" +) + +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", + }, + { + name: "without DNS 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", + }, + } + + 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) + }) + } +} + +func TestHermitInitDNSServer(t *testing.T) { + t.Parallel() + + h := &Hermit{} + + data := types.UnikernelParams{ + Net: types.NetDevParams{ + IP: "10.0.0.2", + Mask: "255.255.255.0", + Gateway: "10.0.0.1", + DNSServer: "1.1.1.1", + }, + } + + err := h.Init(data) + + require.NoError(t, err) + assert.Equal(t, "1.1.1.1", h.Net.DNSServer) +} 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..1d1ce0b1a 100644 --- a/pkg/unikontainers/unikernels/mewz_test.go +++ b/pkg/unikontainers/unikernels/mewz_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/urunc-dev/urunc/pkg/unikontainers/types" ) func TestMewzCommandString(t *testing.T) { @@ -45,6 +46,29 @@ 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", + }, + { + name: "without DNS configured", + mewz: &Mewz{ + Net: MewzNet{ + Address: "10.0.0.2", + Mask: 24, + Gateway: "10.0.0.1", + }, + }, + expected: "ip=10.0.0.2/24 gateway=10.0.0.1", + }, } for _, tc := range testCases { @@ -56,3 +80,23 @@ func TestMewzCommandString(t *testing.T) { }) } } + +func TestMewzInitDNSServer(t *testing.T) { + t.Parallel() + + m := &Mewz{} + + data := types.UnikernelParams{ + Net: types.NetDevParams{ + IP: "10.0.0.2", + Mask: "255.255.255.0", + Gateway: "10.0.0.1", + DNSServer: "1.1.1.1", + }, + } + + err := m.Init(data) + + require.NoError(t, err) + assert.Equal(t, "1.1.1.1", m.Net.DNSServer) +} diff --git a/pkg/unikontainers/unikernels/mirage.go b/pkg/unikontainers/unikernels/mirage.go index ecd73c8bd..4aec61f49 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,6 +44,13 @@ type MirageBlock struct { } func (m *Mirage) CommandString() (string, error) { + if m.Net.DNSServer != "" { + return fmt.Sprintf("%s %s --dns-servers=%s %s", m.Net.Address, + m.Net.Gateway, + m.Net.DNSServer, + m.Command), nil + } + return fmt.Sprintf("%s %s %s", m.Net.Address, m.Net.Gateway, m.Command), nil @@ -124,6 +132,7 @@ func (m *Mirage) Init(data types.UnikernelParams) error { 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 { newBlk := MirageBlock{ diff --git a/pkg/unikontainers/unikernels/mirage_test.go b/pkg/unikontainers/unikernels/mirage_test.go index 7a01b8a23..21492fe3f 100644 --- a/pkg/unikontainers/unikernels/mirage_test.go +++ b/pkg/unikontainers/unikernels/mirage_test.go @@ -125,3 +125,42 @@ func TestMirageBlkDevName(t *testing.T) { assert.Equal(t, "storage", args[0].ID) }) } + +func TestMirageDNS(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: "1.1.1.1", + }, + }) + + assert.NoError(t, err) + + cmd, err := m.CommandString() + assert.NoError(t, err) + assert.Contains(t, cmd, "--dns-servers=1.1.1.1") +} + +func TestMirageDNSMissing(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", + }, + }) + + assert.NoError(t, err) + + cmd, err := m.CommandString() + assert.NoError(t, err) + assert.NotContains(t, cmd, "--dns-servers=") +} From e5e65732521f41cd380b19e1e15c6f4d74817587 Mon Sep 17 00:00:00 2001 From: slash Date: Wed, 30 Sep 2026 18:40:51 +0530 Subject: [PATCH 2/5] chore: add slash-init to contributors Signed-off-by: slash --- .github/contributors.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/contributors.yaml b/.github/contributors.yaml index c3b115b94..e6e4096c5 100644 --- a/.github/contributors.yaml +++ b/.github/contributors.yaml @@ -149,3 +149,6 @@ users: Nachiket-Roy: name: Nachiket Roy email: nachiket.roy.2@gmail.com + slash-init: + name: Gaurav Verma + email: amvermagaurav007@gmail.com From 88ec7da1e125730dd6506ac8238d17a0a54c37b0 Mon Sep 17 00:00:00 2001 From: slash Date: Wed, 30 Sep 2026 18:41:20 +0530 Subject: [PATCH 3/5] fix: address unikernel review feedback Signed-off-by: slash --- pkg/unikontainers/unikernels/hermit_rs_test.go | 11 ----------- pkg/unikontainers/unikernels/mirage.go | 12 ++++-------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/pkg/unikontainers/unikernels/hermit_rs_test.go b/pkg/unikontainers/unikernels/hermit_rs_test.go index 01ce0a9c7..196a5c605 100644 --- a/pkg/unikontainers/unikernels/hermit_rs_test.go +++ b/pkg/unikontainers/unikernels/hermit_rs_test.go @@ -44,17 +44,6 @@ func TestHermitCommandString(t *testing.T) { }, expected: "ip=10.0.0.2/24 gateway=10.0.0.1 env=HERMIT_DNS1=1.1.1.1", }, - { - name: "without DNS 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", - }, } for _, tc := range testCases { diff --git a/pkg/unikontainers/unikernels/mirage.go b/pkg/unikontainers/unikernels/mirage.go index 4aec61f49..b84edf5c1 100644 --- a/pkg/unikontainers/unikernels/mirage.go +++ b/pkg/unikontainers/unikernels/mirage.go @@ -44,16 +44,12 @@ type MirageBlock struct { } func (m *Mirage) CommandString() (string, error) { + command := fmt.Sprintf("%s %s", m.Net.Address, m.Net.Gateway) if m.Net.DNSServer != "" { - return fmt.Sprintf("%s %s --dns-servers=%s %s", m.Net.Address, - m.Net.Gateway, - m.Net.DNSServer, - m.Command), nil + command += fmt.Sprintf(" --dns-servers=%s", m.Net.DNSServer) } - return fmt.Sprintf("%s %s %s", m.Net.Address, - m.Net.Gateway, - m.Command), nil + return command + " " + m.Command, nil } func (m *Mirage) SupportsBlock() bool { @@ -131,8 +127,8 @@ 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.Net.DNSServer = data.Net.DNSServer m.Block = make([]MirageBlock, 0, len(data.Block)) for _, blk := range data.Block { newBlk := MirageBlock{ From f2cf89f82c6dc083e32b8cb009e4a0a95d1abd91 Mon Sep 17 00:00:00 2001 From: slash Date: Fri, 2 Oct 2026 12:49:27 +0530 Subject: [PATCH 4/5] fix: gate guest DNS configuration on DNS client support Signed-off-by: slash --- pkg/unikontainers/annotations.go | 22 ++++++ pkg/unikontainers/annotations_test.go | 6 ++ pkg/unikontainers/monitor_spec_test.go | 14 ++++ pkg/unikontainers/types/types.go | 1 + .../unikernels/hermit_rs_test.go | 21 ----- pkg/unikontainers/unikernels/mewz_test.go | 32 -------- pkg/unikontainers/unikernels/mirage.go | 6 +- pkg/unikontainers/unikernels/mirage_test.go | 77 +++++++++++-------- pkg/unikontainers/unikontainers.go | 4 + 9 files changed, 96 insertions(+), 87 deletions(-) diff --git a/pkg/unikontainers/annotations.go b/pkg/unikontainers/annotations.go index 6b577b872..565453cdf 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" + annotDNSClient = "com.urunc.unikernel.dnsClient" 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"` + DNSClient string `json:"com.urunc.unikernel.dnsClient,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] + dnsClient := spec.Annotations[annotDNSClient] 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, + "dnsClient": dnsClient, "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, + DNSClient: dnsClient, 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), + "dnsClient": tryDecode(conf.DNSClient), }).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.DNSClient) + if err != nil { + return fmt.Errorf("failed to decode dnsClient: %v", err) + } + c.DNSClient = string(decoded) + return nil } @@ -369,6 +381,9 @@ func (c *UnikernelConfig) Map() map[string]string { if c.BlkDev != "" { myMap[annotBlkDev] = c.BlkDev } + if c.DNSClient != "" { + myMap[annotDNSClient] = c.DNSClient + } if c.VAccel != "" { myMap[annotVAccel] = c.VAccel } @@ -423,6 +438,13 @@ func (c *UnikernelConfig) validateValues() error { } } + if c.DNSClient != "" { + _, err = strconv.ParseBool(c.DNSClient) + if err != nil { + return fmt.Errorf("invalid value %q for %s: expected a boolean: %w", c.DNSClient, annotDNSClient, 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..3d15bc210 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", + annotDNSClient: "true", }, } @@ -55,6 +56,7 @@ func TestGetConfigFromSpec(t *testing.T) { MountRootfs: "true", NetDev: "management", BlkDev: "database", + DNSClient: "true", } config := getConfigFromSpec(spec) @@ -240,6 +242,7 @@ func TestMap(t *testing.T) { MountRootfs: "false", NetDev: "netdev_value", BlkDev: "blkdev_value", + DNSClient: "true", VAccel: "vsock", RPCAddress: "vsock://2:1234", } @@ -253,6 +256,7 @@ func TestMap(t *testing.T) { annotMountRootfs: "false", annotNetDev: "netdev_value", annotBlkDev: "blkdev_value", + annotDNSClient: "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, ""}, + {"DNS client enabled", annotDNSClient, "true"}, } for _, tc := range accepted { @@ -362,6 +367,7 @@ func TestValidateValues(t *testing.T) { val string }{ {"non boolean mountRootfs", annotMountRootfs, "yes"}, + {"non boolean DNS client", annotDNSClient, "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..7024df863 100644 --- a/pkg/unikontainers/monitor_spec_test.go +++ b/pkg/unikontainers/monitor_spec_test.go @@ -97,12 +97,26 @@ func TestWriteMonitorSpec(t *testing.T) { assert.Equal(t, "qemu", got.MonitorType) assert.Equal(t, u.UruncCfg.Monitors["qemu"], got.MonitorCfg) assert.Equal(t, specs.User{UID: 1000, GID: 1000}, got.User) + assert.False(t, got.GuestParams.DNSClient) // No knative annotation, so the network type is dynamic. assert.Equal(t, "dynamic", got.NetworkType) // The post-pivot process sees the monitor rootfs as "/". assert.Equal(t, "/", got.GuestParams.Rootfs.MonRootfs) }) + t.Run("passes the DNS client annotation to guest params", func(t *testing.T) { + t.Parallel() + monRootfs := t.TempDir() + u, rootfsParams := newSpecUnikontainer(t, monRootfs) + u.State.Annotations[annotDNSClient] = "true" + + err := u.writeMonitorSpec(rootfsParams, monitorResources{}) + require.NoError(t, err) + + got := readMonitorSpecFile(t, monRootfs) + assert.True(t, got.GuestParams.DNSClient) + }) + 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/types/types.go b/pkg/unikontainers/types/types.go index de1714f62..e64419e19 100644 --- a/pkg/unikontainers/types/types.go +++ b/pkg/unikontainers/types/types.go @@ -96,6 +96,7 @@ type UnikernelParams struct { InitrdPath string // The path to the initrd of the unikernel NetDevName string // The name of the guest network device declared at build time BlkDevName string // The name of the guest block device declared at build time + DNSClient bool // Whether the guest includes a DNS client device Net NetDevParams Block []BlockDevParams Rootfs RootfsParams // Information about rootfs diff --git a/pkg/unikontainers/unikernels/hermit_rs_test.go b/pkg/unikontainers/unikernels/hermit_rs_test.go index 196a5c605..a1ac6cfc5 100644 --- a/pkg/unikontainers/unikernels/hermit_rs_test.go +++ b/pkg/unikontainers/unikernels/hermit_rs_test.go @@ -5,7 +5,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/urunc-dev/urunc/pkg/unikontainers/types" ) func TestHermitCommandString(t *testing.T) { @@ -55,23 +54,3 @@ func TestHermitCommandString(t *testing.T) { }) } } - -func TestHermitInitDNSServer(t *testing.T) { - t.Parallel() - - h := &Hermit{} - - data := types.UnikernelParams{ - Net: types.NetDevParams{ - IP: "10.0.0.2", - Mask: "255.255.255.0", - Gateway: "10.0.0.1", - DNSServer: "1.1.1.1", - }, - } - - err := h.Init(data) - - require.NoError(t, err) - assert.Equal(t, "1.1.1.1", h.Net.DNSServer) -} diff --git a/pkg/unikontainers/unikernels/mewz_test.go b/pkg/unikontainers/unikernels/mewz_test.go index 1d1ce0b1a..d50ae88c4 100644 --- a/pkg/unikontainers/unikernels/mewz_test.go +++ b/pkg/unikontainers/unikernels/mewz_test.go @@ -19,7 +19,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/urunc-dev/urunc/pkg/unikontainers/types" ) func TestMewzCommandString(t *testing.T) { @@ -58,17 +57,6 @@ func TestMewzCommandString(t *testing.T) { }, expected: "ip=10.0.0.2/24 gateway=10.0.0.1 dns=1.1.1.1", }, - { - name: "without DNS configured", - mewz: &Mewz{ - Net: MewzNet{ - Address: "10.0.0.2", - Mask: 24, - Gateway: "10.0.0.1", - }, - }, - expected: "ip=10.0.0.2/24 gateway=10.0.0.1", - }, } for _, tc := range testCases { @@ -80,23 +68,3 @@ func TestMewzCommandString(t *testing.T) { }) } } - -func TestMewzInitDNSServer(t *testing.T) { - t.Parallel() - - m := &Mewz{} - - data := types.UnikernelParams{ - Net: types.NetDevParams{ - IP: "10.0.0.2", - Mask: "255.255.255.0", - Gateway: "10.0.0.1", - DNSServer: "1.1.1.1", - }, - } - - err := m.Init(data) - - require.NoError(t, err) - assert.Equal(t, "1.1.1.1", m.Net.DNSServer) -} diff --git a/pkg/unikontainers/unikernels/mirage.go b/pkg/unikontainers/unikernels/mirage.go index b84edf5c1..676c75d0a 100644 --- a/pkg/unikontainers/unikernels/mirage.go +++ b/pkg/unikontainers/unikernels/mirage.go @@ -27,6 +27,7 @@ type Mirage struct { Command string Monitor string Net MirageNet + DNSClient bool Block []MirageBlock netDevName string blkDevName string @@ -45,8 +46,8 @@ type MirageBlock struct { func (m *Mirage) CommandString() (string, error) { command := fmt.Sprintf("%s %s", m.Net.Address, m.Net.Gateway) - if m.Net.DNSServer != "" { - command += fmt.Sprintf(" --dns-servers=%s", m.Net.DNSServer) + if m.DNSClient && m.Net.DNSServer != "" { + command += fmt.Sprintf(" --dns-servers=udp:%s", m.Net.DNSServer) } return command + " " + m.Command, nil @@ -128,6 +129,7 @@ func (m *Mirage) Init(data types.UnikernelParams) error { m.Net.Gateway = "--ipv4-gateway=" + data.Net.Gateway } m.Net.DNSServer = data.Net.DNSServer + m.DNSClient = data.DNSClient } 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 21492fe3f..fd43c549d 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" ) @@ -127,40 +128,52 @@ func TestMirageBlkDevName(t *testing.T) { } func TestMirageDNS(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: "1.1.1.1", + testCases := []struct { + name string + dnsServer string + dnsClient bool + expected string + }{ + { + name: "DNS client enabled", + dnsServer: "1.1.1.1", + dnsClient: true, + expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 --dns-servers=udp:1.1.1.1 app", }, - }) - - assert.NoError(t, err) - - cmd, err := m.CommandString() - assert.NoError(t, err) - assert.Contains(t, cmd, "--dns-servers=1.1.1.1") -} - -func TestMirageDNSMissing(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", + { + name: "DNS client disabled", + dnsServer: "1.1.1.1", + expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 app", }, - }) + { + name: "DNS server missing with DNS client enabled", + dnsClient: true, + expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 app", + }, + { + name: "DNS server missing with DNS client disabled", + expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 app", + }, + } - assert.NoError(t, err) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := newMirage() + err := m.Init(types.UnikernelParams{ + CmdLine: []string{"app"}, + DNSClient: tc.dnsClient, + 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() - assert.NoError(t, err) - assert.NotContains(t, cmd, "--dns-servers=") + 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..bc98301dd 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -25,6 +25,7 @@ import ( "os" "path/filepath" "runtime" + "strconv" "strings" "sync" "syscall" @@ -542,6 +543,9 @@ func (u *Unikontainer) buildMonitorSpec(rootfsParams types.RootfsParams, monRes Rootfs: rootfsParams, Block: monRes.BlockArgs, } + if dnsClientValue := u.State.Annotations[annotDNSClient]; dnsClientValue != "" { + guest.DNSClient, _ = strconv.ParseBool(dnsClientValue) + } vmmArgs.Sharedfs = monRes.Sharedfs From 87d8888a145416980d52087ecaa61eeaa88a6462 Mon Sep 17 00:00:00 2001 From: slash Date: Fri, 2 Oct 2026 22:30:08 +0530 Subject: [PATCH 5/5] fix: advertise DNS over TCP and UDP for MirageOS Signed-off-by: slash --- .github/contributors.yaml | 3 -- docs/package/index.md | 4 +++ pkg/unikontainers/annotations.go | 28 +++++++-------- pkg/unikontainers/annotations_test.go | 12 +++---- pkg/unikontainers/monitor_spec_test.go | 38 +++++++++++++++------ pkg/unikontainers/types/types.go | 1 - pkg/unikontainers/unikernels/mirage.go | 6 ++-- pkg/unikontainers/unikernels/mirage_test.go | 21 +++--------- pkg/unikontainers/unikontainers.go | 9 +++-- 9 files changed, 61 insertions(+), 61 deletions(-) diff --git a/.github/contributors.yaml b/.github/contributors.yaml index e6e4096c5..c3b115b94 100644 --- a/.github/contributors.yaml +++ b/.github/contributors.yaml @@ -149,6 +149,3 @@ users: Nachiket-Roy: name: Nachiket Roy email: nachiket.roy.2@gmail.com - slash-init: - name: Gaurav Verma - email: amvermagaurav007@gmail.com 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 565453cdf..f82d3b634 100644 --- a/pkg/unikontainers/annotations.go +++ b/pkg/unikontainers/annotations.go @@ -51,7 +51,7 @@ const ( annotMountRootfs = "com.urunc.unikernel.mountRootfs" annotNetDev = "com.urunc.unikernel.solo5NetDev" annotBlkDev = "com.urunc.unikernel.solo5BlkDev" - annotDNSClient = "com.urunc.unikernel.dnsClient" + annotAdvertiseDNS = "com.urunc.unikernel.advertiseDNS" annotVAccel = "com.urunc.unikernel.vAccel" annotRPCAddress = "com.urunc.unikernel.RPCAddress" ) @@ -104,7 +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"` - DNSClient string `json:"com.urunc.unikernel.dnsClient,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. @@ -190,7 +190,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { MountRootfs := spec.Annotations[annotMountRootfs] netDev := spec.Annotations[annotNetDev] blkDev := spec.Annotations[annotBlkDev] - dnsClient := spec.Annotations[annotDNSClient] + advertiseDNS := spec.Annotations[annotAdvertiseDNS] vAccel := spec.Annotations[annotVAccel] rpcAddress := spec.Annotations[annotRPCAddress] uniklog.WithFields(logrus.Fields{ @@ -204,7 +204,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { "mountRootfs": MountRootfs, "netDev": netDev, "blkDev": blkDev, - "dnsClient": dnsClient, + "advertiseDNS": advertiseDNS, "vAccel": vAccel, "rpcAddress": rpcAddress, }).WithField("source", "spec").Debug("urunc annotations") @@ -220,7 +220,7 @@ func getConfigFromSpec(spec *specs.Spec) *UnikernelConfig { MountRootfs: MountRootfs, NetDev: netDev, BlkDev: blkDev, - DNSClient: dnsClient, + AdvertiseDNS: advertiseDNS, VAccel: vAccel, RPCAddress: rpcAddress, } @@ -263,7 +263,7 @@ func getConfigFromJSON(jsonFilePath string) (*UnikernelConfig, error) { "mountRootfs": tryDecode(conf.MountRootfs), "netDev": tryDecode(conf.NetDev), "blkDev": tryDecode(conf.BlkDev), - "dnsClient": tryDecode(conf.DNSClient), + "advertiseDNS": tryDecode(conf.AdvertiseDNS), }).WithField("source", uruncJSONFilename).Debug("urunc annotations") return &conf, nil @@ -339,11 +339,11 @@ func (c *UnikernelConfig) decode() error { } c.BlkDev = string(decoded) - decoded, err = base64.StdEncoding.DecodeString(c.DNSClient) + decoded, err = base64.StdEncoding.DecodeString(c.AdvertiseDNS) if err != nil { - return fmt.Errorf("failed to decode dnsClient: %v", err) + return fmt.Errorf("failed to decode advertiseDNS: %v", err) } - c.DNSClient = string(decoded) + c.AdvertiseDNS = string(decoded) return nil } @@ -381,8 +381,8 @@ func (c *UnikernelConfig) Map() map[string]string { if c.BlkDev != "" { myMap[annotBlkDev] = c.BlkDev } - if c.DNSClient != "" { - myMap[annotDNSClient] = c.DNSClient + if c.AdvertiseDNS != "" { + myMap[annotAdvertiseDNS] = c.AdvertiseDNS } if c.VAccel != "" { myMap[annotVAccel] = c.VAccel @@ -438,10 +438,10 @@ func (c *UnikernelConfig) validateValues() error { } } - if c.DNSClient != "" { - _, err = strconv.ParseBool(c.DNSClient) + if c.AdvertiseDNS != "" { + _, err = strconv.ParseBool(c.AdvertiseDNS) if err != nil { - return fmt.Errorf("invalid value %q for %s: expected a boolean: %w", c.DNSClient, annotDNSClient, err) + return fmt.Errorf("invalid value %q for %s: expected a boolean: %w", c.AdvertiseDNS, annotAdvertiseDNS, err) } } diff --git a/pkg/unikontainers/annotations_test.go b/pkg/unikontainers/annotations_test.go index 3d15bc210..5c68c2044 100644 --- a/pkg/unikontainers/annotations_test.go +++ b/pkg/unikontainers/annotations_test.go @@ -42,7 +42,7 @@ func TestGetConfigFromSpec(t *testing.T) { annotMountRootfs: "true", annotNetDev: "management", annotBlkDev: "database", - annotDNSClient: "true", + annotAdvertiseDNS: "true", }, } @@ -56,7 +56,7 @@ func TestGetConfigFromSpec(t *testing.T) { MountRootfs: "true", NetDev: "management", BlkDev: "database", - DNSClient: "true", + AdvertiseDNS: "true", } config := getConfigFromSpec(spec) @@ -242,7 +242,7 @@ func TestMap(t *testing.T) { MountRootfs: "false", NetDev: "netdev_value", BlkDev: "blkdev_value", - DNSClient: "true", + AdvertiseDNS: "true", VAccel: "vsock", RPCAddress: "vsock://2:1234", } @@ -256,7 +256,7 @@ func TestMap(t *testing.T) { annotMountRootfs: "false", annotNetDev: "netdev_value", annotBlkDev: "blkdev_value", - annotDNSClient: "true", + annotAdvertiseDNS: "true", annotVAccel: "vsock", annotRPCAddress: "vsock://2:1234", } @@ -350,7 +350,7 @@ func TestValidateValues(t *testing.T) { {"solo5 device", annotNetDev, "management"}, {"empty solo5 device", annotNetDev, ""}, {"empty mountRootfs", annotMountRootfs, ""}, - {"DNS client enabled", annotDNSClient, "true"}, + {"advertise DNS enabled", annotAdvertiseDNS, "true"}, } for _, tc := range accepted { @@ -367,7 +367,7 @@ func TestValidateValues(t *testing.T) { val string }{ {"non boolean mountRootfs", annotMountRootfs, "yes"}, - {"non boolean DNS client", annotDNSClient, "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 7024df863..9f99d3b0b 100644 --- a/pkg/unikontainers/monitor_spec_test.go +++ b/pkg/unikontainers/monitor_spec_test.go @@ -97,24 +97,40 @@ func TestWriteMonitorSpec(t *testing.T) { assert.Equal(t, "qemu", got.MonitorType) assert.Equal(t, u.UruncCfg.Monitors["qemu"], got.MonitorCfg) assert.Equal(t, specs.User{UID: 1000, GID: 1000}, got.User) - assert.False(t, got.GuestParams.DNSClient) // No knative annotation, so the network type is dynamic. assert.Equal(t, "dynamic", got.NetworkType) // The post-pivot process sees the monitor rootfs as "/". assert.Equal(t, "/", got.GuestParams.Rootfs.MonRootfs) }) - t.Run("passes the DNS client annotation to guest params", func(t *testing.T) { + t.Run("filters the DNS server based on the advertise DNS annotation", func(t *testing.T) { t.Parallel() - monRootfs := t.TempDir() - u, rootfsParams := newSpecUnikontainer(t, monRootfs) - u.State.Annotations[annotDNSClient] = "true" - - err := u.writeMonitorSpec(rootfsParams, monitorResources{}) - require.NoError(t, err) - - got := readMonitorSpecFile(t, monRootfs) - assert.True(t, got.GuestParams.DNSClient) + 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) { diff --git a/pkg/unikontainers/types/types.go b/pkg/unikontainers/types/types.go index e64419e19..de1714f62 100644 --- a/pkg/unikontainers/types/types.go +++ b/pkg/unikontainers/types/types.go @@ -96,7 +96,6 @@ type UnikernelParams struct { InitrdPath string // The path to the initrd of the unikernel NetDevName string // The name of the guest network device declared at build time BlkDevName string // The name of the guest block device declared at build time - DNSClient bool // Whether the guest includes a DNS client device Net NetDevParams Block []BlockDevParams Rootfs RootfsParams // Information about rootfs diff --git a/pkg/unikontainers/unikernels/mirage.go b/pkg/unikontainers/unikernels/mirage.go index 676c75d0a..023ca5a49 100644 --- a/pkg/unikontainers/unikernels/mirage.go +++ b/pkg/unikontainers/unikernels/mirage.go @@ -27,7 +27,6 @@ type Mirage struct { Command string Monitor string Net MirageNet - DNSClient bool Block []MirageBlock netDevName string blkDevName string @@ -46,8 +45,8 @@ type MirageBlock struct { func (m *Mirage) CommandString() (string, error) { command := fmt.Sprintf("%s %s", m.Net.Address, m.Net.Gateway) - if m.DNSClient && m.Net.DNSServer != "" { - command += fmt.Sprintf(" --dns-servers=udp:%s", m.Net.DNSServer) + if m.Net.DNSServer != "" { + command += fmt.Sprintf(" --dns-servers=udp:%s,tcp:%s", m.Net.DNSServer, m.Net.DNSServer) } return command + " " + m.Command, nil @@ -129,7 +128,6 @@ func (m *Mirage) Init(data types.UnikernelParams) error { m.Net.Gateway = "--ipv4-gateway=" + data.Net.Gateway } m.Net.DNSServer = data.Net.DNSServer - m.DNSClient = data.DNSClient } 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 fd43c549d..e5c36395c 100644 --- a/pkg/unikontainers/unikernels/mirage_test.go +++ b/pkg/unikontainers/unikernels/mirage_test.go @@ -131,27 +131,15 @@ func TestMirageDNS(t *testing.T) { testCases := []struct { name string dnsServer string - dnsClient bool expected string }{ { - name: "DNS client enabled", + name: "DNS server present", dnsServer: "1.1.1.1", - dnsClient: true, - expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 --dns-servers=udp:1.1.1.1 app", + 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 client disabled", - dnsServer: "1.1.1.1", - expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 app", - }, - { - name: "DNS server missing with DNS client enabled", - dnsClient: true, - expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 app", - }, - { - name: "DNS server missing with DNS client disabled", + name: "DNS server absent", expected: "--ipv4=10.0.0.2/24 --ipv4-gateway=10.0.0.1 app", }, } @@ -160,8 +148,7 @@ func TestMirageDNS(t *testing.T) { t.Run(tc.name, func(t *testing.T) { m := newMirage() err := m.Init(types.UnikernelParams{ - CmdLine: []string{"app"}, - DNSClient: tc.dnsClient, + CmdLine: []string{"app"}, Net: types.NetDevParams{ IP: "10.0.0.2", Mask: "255.255.255.0", diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index bc98301dd..21ac4df6d 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -543,9 +543,6 @@ func (u *Unikontainer) buildMonitorSpec(rootfsParams types.RootfsParams, monRes Rootfs: rootfsParams, Block: monRes.BlockArgs, } - if dnsClientValue := u.State.Annotations[annotDNSClient]; dnsClientValue != "" { - guest.DNSClient, _ = strconv.ParseBool(dnsClientValue) - } vmmArgs.Sharedfs = monRes.Sharedfs @@ -558,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 }