diff --git a/.github/contributors.yaml b/.github/contributors.yaml index afc781fa3..61ed62103 100644 --- a/.github/contributors.yaml +++ b/.github/contributors.yaml @@ -134,3 +134,6 @@ users: magic-peach: name: Akanksha Trehun email: akankshatrehun@gmail.com + alimx07: + name: Ali Mohamed + email: amx746@gmail.com diff --git a/pkg/unikontainers/types/types.go b/pkg/unikontainers/types/types.go index f5d668b0d..e66bd2bf2 100644 --- a/pkg/unikontainers/types/types.go +++ b/pkg/unikontainers/types/types.go @@ -45,12 +45,13 @@ type VMM interface { } type NetDevParams struct { - IP string // The veth device IP - Mask string // The veth device mask - Gateway string // The veth device gateway - MAC string // The MAC address of the guest network device - TapDev string // The tap device name - MTU int // The MTU value of the tap device + IP string // The veth device IP + Mask string // The veth device mask + Gateway string // The veth device gateway + MAC string // The MAC address of the guest network device + TapDev string // The tap device name + MTU int // The MTU value of the tap device + DNSServer string // The nameserver of the container, empty if there is none } type BlockDevParams struct { diff --git a/pkg/unikontainers/unikernels/unikraft.go b/pkg/unikontainers/unikernels/unikraft.go index 7bc02a7ad..2ad5fb435 100644 --- a/pkg/unikontainers/unikernels/unikraft.go +++ b/pkg/unikontainers/unikernels/unikraft.go @@ -27,6 +27,8 @@ import ( const UnikraftUnikernel string = "unikraft" const UnikraftCompatVersion string = "0.16.1" +const defaultDNSServer string = "8.8.8.8" + var ErrUndefinedVersion = errors.New("version is undefined, using default version") var ErrVersionParsing = errors.New("failed to parse provided version, using default version") @@ -107,10 +109,14 @@ func (u *Unikraft) Init(data types.UnikernelParams) error { u.Monitor = data.Monitor u.Command = strings.Join(data.CmdLine, " ") - return u.configureUnikraftArgs(data.Rootfs.Type, data.Net.IP, data.Net.Gateway, data.Net.Mask) + return u.configureUnikraftArgs(data.Rootfs.Type, data.Net.IP, data.Net.Gateway, data.Net.Mask, data.Net.DNSServer) } -func (u *Unikraft) configureUnikraftArgs(rootFsType, ethDeviceIP, ethDeviceGateway, ethDeviceMask string) error { +func (u *Unikraft) configureUnikraftArgs(rootFsType, ethDeviceIP, ethDeviceGateway, ethDeviceMask, dnsServer string) error { + if dnsServer == "" { + dnsServer = defaultDNSServer + } + setCompatArgs := func() { u.Net.Address = "netdev.ipv4_addr=" + ethDeviceIP u.Net.Gateway = "netdev.ipv4_gw_addr=" + ethDeviceGateway @@ -125,7 +131,7 @@ func (u *Unikraft) configureUnikraftArgs(rootFsType, ethDeviceIP, ethDeviceGatew } setCurrentArgs := func() { - u.Net.Address = "netdev.ip=" + ethDeviceIP + "/24:" + ethDeviceGateway + ":8.8.8.8" + u.Net.Address = "netdev.ip=" + ethDeviceIP + "/24:" + ethDeviceGateway + ":" + dnsServer switch rootFsType { case "initrd": // TODO: This needs better handling. We need to revisit this diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index 2e5800eff..cd688aa0a 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -266,7 +266,7 @@ func (u *Unikontainer) SetRunningState() error { // SetupNet creates the sandbox's network device (tap) in the current network // namespace and returns its parameters; uid and gid own the tap device. -func SetupNet(networkType string, uid, gid uint32) (types.NetDevParams, error) { +func SetupNet(networkType string, mounts []specs.Mount, uid, gid uint32) (types.NetDevParams, error) { uniklog.WithField("network type", networkType).Debug("Retrieved network type") netArgs := types.NetDevParams{} netManager, err := network.NewNetworkManager(networkType) @@ -292,6 +292,7 @@ func SetupNet(networkType string, uid, gid uint32) (types.NetDevParams, error) { // virtual ethernet interface inside the namespace netArgs.MAC = networkInfo.EthDevice.MAC netArgs.MTU = networkInfo.EthDevice.MTU + netArgs.DNSServer = getDNSServer(mounts) } return netArgs, nil @@ -618,7 +619,7 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { } // handle network - netArgs, err := SetupNet(u.getNetworkType(), u.Spec.Process.User.UID, u.Spec.Process.User.GID) + netArgs, err := SetupNet(u.getNetworkType(), u.Spec.Mounts, u.Spec.Process.User.UID, u.Spec.Process.User.GID) if err != nil { uniklog.Errorf("failed to setup network: %v", err) return err diff --git a/pkg/unikontainers/utils.go b/pkg/unikontainers/utils.go index 68977dd0d..b185377f0 100644 --- a/pkg/unikontainers/utils.go +++ b/pkg/unikontainers/utils.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "net" "os" "os/exec" "path/filepath" @@ -388,3 +389,35 @@ func executeHook(hook specs.Hook, state []byte) error { return nil } + +func getDNSServer(mounts []specs.Mount) string { + resolvConf := "" + for _, mount := range mounts { + if filepath.Clean(mount.Destination) == "/etc/resolv.conf" { + resolvConf = mount.Source + break + } + } + if resolvConf == "" { + return "" + } + + data, err := os.ReadFile(resolvConf) + if err != nil { + uniklog.Warnf("Failed to read %s: %v", resolvConf, err) + return "" + } + + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) < 2 || fields[0] != "nameserver" { + continue + } + addr := net.ParseIP(fields[1]) + if addr != nil && addr.To4() != nil && !addr.IsLoopback() { + return addr.String() + } + } + + return "" +} diff --git a/pkg/unikontainers/utils_test.go b/pkg/unikontainers/utils_test.go index 2d23ea335..6ecf32ae4 100644 --- a/pkg/unikontainers/utils_test.go +++ b/pkg/unikontainers/utils_test.go @@ -340,3 +340,80 @@ func TestLoadSpec(t *testing.T) { assert.Contains(t, err.Error(), "failed to parse specification json", "Expected specific error message") }) } + +func TestGetDNSServer(t *testing.T) { + tests := []struct { + name string + content string + expected string + }{ + { + name: "single nameserver", + content: "nameserver 10.96.0.10\n", + expected: "10.96.0.10", + }, + { + name: "first nameserver is used", + content: "search svc.cluster.local\nnameserver 10.96.0.10\nnameserver 8.8.4.4\noptions ndots:5\n", + expected: "10.96.0.10", + }, + { + name: "comments are ignored", + content: "# nameserver 1.1.1.1\n\n nameserver\t192.168.1.1 \n", + expected: "192.168.1.1", + }, + { + name: "loopback nameserver is skipped", + content: "nameserver 127.0.0.11\nnameserver 1.1.1.1\n", + expected: "1.1.1.1", + }, + { + name: "IPv6 nameserver is skipped", + content: "nameserver fd00::1\nnameserver 1.1.1.1\n", + expected: "1.1.1.1", + }, + { + name: "invalid entries are skipped", + content: "nameserver\nnameserver not-an-ip\nnameserver 1.1.1.1\n", + expected: "1.1.1.1", + }, + { + name: "no usable nameserver", + content: "search svc.cluster.local\nnameserver 127.0.0.53\n", + expected: "", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + resolvConf := filepath.Join(t.TempDir(), "resolv.conf") + err := os.WriteFile(resolvConf, []byte(tc.content), 0600) + assert.NoError(t, err) + mounts := []specs.Mount{ + {Destination: "/etc/hostname", Source: "/dummy/hostname"}, + {Destination: "/etc/resolv.conf", Source: resolvConf}, + } + + assert.Equal(t, tc.expected, getDNSServer(mounts)) + }) + } + + t.Run("no resolv.conf mount", func(t *testing.T) { + t.Parallel() + mounts := []specs.Mount{ + {Destination: "/etc/hostname", Source: "/dummy/hostname"}, + } + + assert.Equal(t, "", getDNSServer(mounts)) + }) + + t.Run("missing resolv.conf file", func(t *testing.T) { + t.Parallel() + mounts := []specs.Mount{ + {Destination: "/etc/resolv.conf", Source: filepath.Join(t.TempDir(), "resolv.conf")}, + } + + assert.Equal(t, "", getDNSServer(mounts)) + }) +} diff --git a/tests/e2e/test_cases.go b/tests/e2e/test_cases.go index 3b90a381e..db2348da9 100644 --- a/tests/e2e/test_cases.go +++ b/tests/e2e/test_cases.go @@ -1241,5 +1241,22 @@ func dockerTestCases() []containerTestArgs { Skippable: false, TestFunc: namespaceTest, }, + { + Image: "harbor.nbfc.io/nubificus/urunc/dns-test-qemu-unikraft-initrd:latest", + Name: "Qemu-unikraft-dns-external", + Devmapper: false, + Seccomp: true, + UID: 0, + GID: 0, + Groups: []int64{}, + Memory: "", + Cli: "", + Volumes: []containerVolume{}, + StaticNet: false, + SideContainers: []string{}, + Skippable: true, + ExpectOut: "github.com OK", + TestFunc: matchTest, + }, } }