diff --git a/.github/contributors.yaml b/.github/contributors.yaml index 411f9f42..400b3b8d 100644 --- a/.github/contributors.yaml +++ b/.github/contributors.yaml @@ -107,3 +107,6 @@ users: Chennamma-Hotkar: name: Chennamma Hotkar email: channuhotkar@gmail.com + alimx07: + name: Ali Mohamed + email: amx746@gmail.com diff --git a/internal/constants/network_constants.go b/internal/constants/network_constants.go index de8a30cc..640fe857 100644 --- a/internal/constants/network_constants.go +++ b/internal/constants/network_constants.go @@ -18,6 +18,7 @@ const ( StaticNetworkTapIP = "172.16.1.1" StaticNetworkUnikernelIP = "172.16.1.2" // TODO: Experiment with DynamicNetworkTapIP starting from 172.16.X.1 - DynamicNetworkTapIP = "172.16.X.2" - QueueProxyRedirectIP = "172.16.1.2" + DynamicNetworkTapIP = "172.16.X.2" + QueueProxyRedirectIP = "172.16.1.2" + LocalhostDNSResolverIP = "192.168.100.100" ) diff --git a/pkg/network/localhost/docker.go b/pkg/network/localhost/docker.go new file mode 100644 index 00000000..7597ce5f --- /dev/null +++ b/pkg/network/localhost/docker.go @@ -0,0 +1,189 @@ +// 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 localhost + +import ( + "bytes" + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/vishvananda/netlink" +) + +const ( + dockerCustomNetDNS = "127.0.0.11" +) + +var ( + // dockerDNSTCPRe and dockerDNSUDPRe extract the tcp/udp ports that Docker's + // embedded DNS resolver (127.0.0.11) listens on. + dockerDNSTCPRe = regexp.MustCompile(`-p\s+tcp.*to-destination\s+127\.0\.0\.11:(\d+)`) + dockerDNSUDPRe = regexp.MustCompile(`-p\s+udp.*to-destination\s+127\.0\.0\.11:(\d+)`) +) + +type dockerLocalhost struct { + resolvConfPath string + resolvIP net.IP +} + +func (dockerLocalhost) Name() string { + return "docker" +} + +func detectDocker(containerID string, dnsIP net.IP) (Forwarder, error) { + containerBase := filepath.Join("/var/lib/docker/containers", containerID) + if _, err := os.Stat(containerBase); err != nil { + if os.IsNotExist(err) { + return nil, nil // not a Docker runtime + } + return nil, err + } + + resolvConfPath := filepath.Join(containerBase, "resolv.conf") + data, err := os.ReadFile(resolvConfPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("resolv.conf is missing: %w", err) + } + return nil, err + } + + // When we run container in custom user network + // Docker add 127.0.0.11 as the embedded DNS resolver + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) >= 2 && fields[0] == "nameserver" && fields[1] == dockerCustomNetDNS { + return dockerLocalhost{ + resolvConfPath: resolvConfPath, + resolvIP: dnsIP, + }, nil + } + } + return nil, nil +} + +func (d dockerLocalhost) Forward(tap, redirect netlink.Link) (ForwardResult, error) { + resolvIP := d.resolvIP + // The guest reuses the container's MAC, so replies must be addressed to it. + guestMAC := redirect.Attrs().HardwareAddr + + path, err := exec.LookPath("iptables") + if err != nil { + return ForwardResult{}, err + } + + tcpPort, udpPort, err := dockerDNSPorts(path) + if err != nil { + return ForwardResult{}, err + } + lhlog.Debugf("Docker DNS resolver: tcp/%s udp/%s", tcpPort, udpPort) + + if err := addDNATRule(path, resolvIP, "udp", udpPort); err != nil { + return ForwardResult{}, err + } + if err := addDNATRule(path, resolvIP, "tcp", tcpPort); err != nil { + return ForwardResult{}, err + } + lhlog.Debug("Applied custom Docker iptables rules for NAT") + + loopback, err := netlink.LinkByName("lo") + if err != nil { + return ForwardResult{}, err + } + if err := addClsactQdisc(loopback); err != nil { + return ForwardResult{}, fmt.Errorf("addClsactQdisc(interface=%s) failed: %w", loopback.Attrs().Name, err) + } + if err := addTapToLoRedirect(tap, resolvIP); err != nil { + return ForwardResult{}, fmt.Errorf("addTapToLoRedirect(%s) failed: %w", tap.Attrs().Name, err) + } + if err := addLoToTapRedirect(tap, resolvIP, guestMAC); err != nil { + return ForwardResult{}, fmt.Errorf("addLoToTapRedirect(%s) failed: %w", tap.Attrs().Name, err) + } + + lhlog.Debug("Applied TC filters for Docker localhost forwarding") + + err = setKernelFlags(map[string]string{ + "/proc/sys/net/ipv4/conf/lo/route_localnet": "1", + fmt.Sprintf("/proc/sys/net/ipv4/conf/%s/accept_local", tap.Attrs().Name): "1", + "/proc/sys/net/ipv4/conf/lo/accept_local": "1", + }) + + if err != nil { + return ForwardResult{}, fmt.Errorf("Failed to set required kernel flags for forwarding: %w", err) + } + + lhlog.Debug("Applied Kernel flags required for packets forwarding") + + return ForwardResult{DNSServer: resolvIP, ResolvConfPath: d.resolvConfPath}, nil +} + +func dockerDNSPorts(path string) (tcpPort, udpPort string, err error) { + var stdout, stderr bytes.Buffer + cmd := exec.Cmd{ + Path: path, + Args: []string{path, "-t", "nat", "-S", "DOCKER_OUTPUT", "--wait", "1"}, + Stdout: &stdout, + Stderr: &stderr, + } + if err := cmd.Run(); err != nil { + if _, ok := err.(*exec.ExitError); ok { + return "", "", fmt.Errorf("iptables command %s failed: %s", cmd.String(), stderr.String()) + } + return "", "", err + } + + out := stdout.String() + if m := dockerDNSTCPRe.FindStringSubmatch(out); m != nil { + tcpPort = m[1] + } + if m := dockerDNSUDPRe.FindStringSubmatch(out); m != nil { + udpPort = m[1] + } + if tcpPort == "" || udpPort == "" { + return "", "", fmt.Errorf("could not find Docker DNS ports in DOCKER_OUTPUT chain") + } + return tcpPort, udpPort, nil +} + +func addDNATRule(path string, resolvIP net.IP, proto, port string) error { + var stdout, stderr bytes.Buffer + cmd := exec.Cmd{ + Path: path, + Args: []string{ + path, "-t", "nat", "-A", "PREROUTING", + "-i", "lo", + "-d", resolvIP.String(), + "-p", proto, + "-j", "DNAT", + "--to-destination", "127.0.0.11:" + port, + "--wait", "1", + }, + Stdout: &stdout, + Stderr: &stderr, + } + if err := cmd.Run(); err != nil { + if _, ok := err.(*exec.ExitError); ok { + lhlog.Debugf("Iptables error %s", err.Error()) + return fmt.Errorf("iptables command %s failed: %s", cmd.String(), stderr.String()) + } + return err + } + return nil +} diff --git a/pkg/network/localhost/localhost.go b/pkg/network/localhost/localhost.go new file mode 100644 index 00000000..645f6cef --- /dev/null +++ b/pkg/network/localhost/localhost.go @@ -0,0 +1,164 @@ +// 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 localhost exposes services listening on the container's loopback +// (e.g. a container runtime's embedded DNS resolver) to the unikernel guest. +// Each supported runtime implements Forwarder; DetectForwarder probes the +// registered runtimes and returns the matching implementation, if any. +package localhost + +import ( + "fmt" + "net" + "os" + + "github.com/sirupsen/logrus" + "github.com/urunc-dev/urunc/internal/constants" + "github.com/vishvananda/netlink" + "golang.org/x/sys/unix" +) + +var lhlog = logrus.WithField("subsystem", "network") + +type Forwarder interface { + Name() string + // Forward installs the host-side plumbing (tc filters + customRules) that exposes + // the loopback service to the guest. + Forward(tap, redirect netlink.Link) (ForwardResult, error) +} + +type ForwardResult struct { + DNSServer net.IP + ResolvConfPath string +} + +// localhostForwarders holds the ordered set of runtime probes; the first with non-nil return wins +var localhostForwarders = []func(containerID string, dnsIP net.IP) (Forwarder, error){ + // we detect docker only for now. + detectDocker, +} + +func DetectForwarder(containerID string, dnsResolverIP string) (Forwarder, error) { + dnsIP := net.ParseIP(dnsResolverIP) + if dnsIP == nil { + lhlog.Warnf("Invalid DNS resolver IP %q, falling back to default %s", dnsResolverIP, constants.LocalhostDNSResolverIP) + dnsIP = net.ParseIP(constants.LocalhostDNSResolverIP) + } + for _, detect := range localhostForwarders { + fwd, err := detect(containerID, dnsIP) + if err != nil { + return nil, err + } + if fwd != nil { + return fwd, nil + } + } + return nil, nil +} + +func uint16Ptr(v uint16) *uint16 { + return &v +} + +func addClsactQdisc(link netlink.Link) error { + clsact := &netlink.Clsact{ + QdiscAttrs: netlink.QdiscAttrs{ + LinkIndex: link.Attrs().Index, + Parent: netlink.HANDLE_CLSACT, + }, + } + return netlink.QdiscAdd((clsact)) +} + +func addTapToLoRedirect(source netlink.Link, resolvIP net.IP) error { + + // tc filter add dev ingress protocol ip pref x \ + // u32 match ip dst \ + // action skbedit ptype host pipe \ + // action mirred ingress redirect dev lo + + return netlink.FilterAdd(&netlink.Flower{ + FilterAttrs: netlink.FilterAttrs{ + LinkIndex: source.Attrs().Index, + Parent: netlink.MakeHandle(0xffff, 0), + Priority: 100, + Protocol: unix.ETH_P_IP, + }, + DestIP: resolvIP, + Actions: []netlink.Action{ + &netlink.SkbEditAction{ + ActionAttrs: netlink.ActionAttrs{ + Action: netlink.TC_ACT_PIPE, + }, + PType: uint16Ptr(unix.PACKET_HOST), + }, &netlink.MirredAction{ + ActionAttrs: netlink.ActionAttrs{ + Action: netlink.TC_ACT_STOLEN, + }, + MirredAction: netlink.TCA_INGRESS_REDIR, + Ifindex: 1, // LOOPBACK + }, + }, + }) +} + +func addLoToTapRedirect(target netlink.Link, resolvIP net.IP, guestMAC net.HardwareAddr) error { + + // tc filter add dev lo egress protocol ip pref x \ + // u32 match ip src \ + // action pedit ex munge eth dst set pipe \ + // action mirred egress redirect dev + + return netlink.FilterAdd(&netlink.Flower{ + FilterAttrs: netlink.FilterAttrs{ + LinkIndex: 1, // LOOPBACK + Parent: netlink.HANDLE_MIN_EGRESS, + Priority: 100, + Protocol: unix.ETH_P_IP, + }, + SrcIP: resolvIP, + Actions: []netlink.Action{ + &netlink.PeditAction{ + ActionAttrs: netlink.ActionAttrs{ + Action: netlink.TC_ACT_PIPE, + }, + DstMacAddr: guestMAC, + }, + &netlink.MirredAction{ + ActionAttrs: netlink.ActionAttrs{ + Action: netlink.TC_ACT_STOLEN, + }, + MirredAction: netlink.TCA_EGRESS_REDIR, + Ifindex: target.Attrs().Index, + }, + }, + }) +} + +func setKernelFlags(flags map[string]string) error { + + for flag, value := range flags { + file, err := os.OpenFile(flag, os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open %s: %w", flag, err) + } + defer file.Close() + + _, err = file.WriteString(value) + if err != nil { + return fmt.Errorf("failed to set flag %s to %s: %w", flag, value, err) + } + } + return nil +} diff --git a/pkg/network/network.go b/pkg/network/network.go index 6fb18c6c..0cac3b67 100644 --- a/pkg/network/network.go +++ b/pkg/network/network.go @@ -23,6 +23,7 @@ import ( "github.com/jackpal/gateway" "github.com/sirupsen/logrus" + "github.com/urunc-dev/urunc/pkg/network/localhost" "github.com/vishvananda/netlink" "golang.org/x/sys/unix" ) @@ -34,11 +35,13 @@ const ( var netlog = logrus.WithField("subsystem", "network") type UnikernelNetworkInfo struct { - TapDevice string - EthDevice Interface + TapDevice string + EthDevice Interface + DNSServer string + ResolvConfPath string } type Manager interface { - NetworkSetup(uid uint32, gid uint32) (*UnikernelNetworkInfo, error) + NetworkSetup(uid uint32, gid uint32, fwd localhost.Forwarder) (*UnikernelNetworkInfo, error) } type Interface struct { diff --git a/pkg/network/network_dynamic.go b/pkg/network/network_dynamic.go index 1d6a2237..5e6f4116 100644 --- a/pkg/network/network_dynamic.go +++ b/pkg/network/network_dynamic.go @@ -18,6 +18,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/urunc-dev/urunc/pkg/network/localhost" ) type DynamicNetwork struct { @@ -32,7 +34,7 @@ type DynamicNetwork struct { // FIXME: CUrrently only one tap device per netns can provide functional networking. We need to find a proper way to handle networking // for multiple unikernels in the same pod/network namespace. // See: https://github.com/urunc-dev/urunc/issues/13 -func (n DynamicNetwork) NetworkSetup(uid uint32, gid uint32) (*UnikernelNetworkInfo, error) { +func (n DynamicNetwork) NetworkSetup(uid uint32, gid uint32, fwd localhost.Forwarder) (*UnikernelNetworkInfo, error) { tapIndex, err := getTapIndex() if err != nil { return nil, fmt.Errorf("getTapIndex failed: %w", err) @@ -62,8 +64,20 @@ func (n DynamicNetwork) NetworkSetup(uid uint32, gid uint32) (*UnikernelNetworkI return nil, fmt.Errorf("getInterfaceInfo(%s) failed: %w", redirectLink.Attrs().Name, err) } - return &UnikernelNetworkInfo{ + info := &UnikernelNetworkInfo{ TapDevice: newTapDevice.Attrs().Name, EthDevice: ifInfo, - }, nil + } + + if fwd != nil { + res, err := fwd.Forward(newTapDevice, redirectLink) + if err != nil { + return nil, fmt.Errorf("%s localhost forwarder failed: %w", fwd.Name(), err) + } + info.DNSServer = res.DNSServer.String() + info.ResolvConfPath = res.ResolvConfPath + netlog.Debugf("%s localhost forwarder applied: resolver IP %s", fwd.Name(), info.DNSServer) + } + + return info, nil } diff --git a/pkg/network/network_static.go b/pkg/network/network_static.go index fa46608c..06a458f9 100644 --- a/pkg/network/network_static.go +++ b/pkg/network/network_static.go @@ -22,6 +22,7 @@ import ( "strings" "github.com/urunc-dev/urunc/internal/constants" + "github.com/urunc-dev/urunc/pkg/network/localhost" ) var StaticIPAddr = fmt.Sprintf("%s/24", constants.StaticNetworkTapIP) @@ -88,7 +89,7 @@ func setNATRule(iface string, sourceIP string) error { return nil } -func (n StaticNetwork) NetworkSetup(uid uint32, gid uint32) (*UnikernelNetworkInfo, error) { +func (n StaticNetwork) NetworkSetup(uid uint32, gid uint32, _ localhost.Forwarder) (*UnikernelNetworkInfo, error) { newTapName := strings.ReplaceAll(DefaultTap, "X", "0") addTCRules := false redirectLink, err := discoverContainerIface() diff --git a/pkg/unikontainers/types/types.go b/pkg/unikontainers/types/types.go index c6388e2c..ff26921f 100644 --- a/pkg/unikontainers/types/types.go +++ b/pkg/unikontainers/types/types.go @@ -45,12 +45,14 @@ 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 // DNSServer is the virtual resolver IP if a DNS fixup installed for the guest + ResolvConfPath string // ResolvConfPath is the host path of the resolv.conf the guest reads. Empty when not applicable. } type BlockDevParams struct { diff --git a/pkg/unikontainers/unikernels/linux.go b/pkg/unikontainers/unikernels/linux.go index d6bb8095..d656b78b 100644 --- a/pkg/unikontainers/unikernels/linux.go +++ b/pkg/unikontainers/unikernels/linux.go @@ -17,6 +17,7 @@ package unikernels import ( "fmt" "net" + "os" "path/filepath" "runtime" "strconv" @@ -215,6 +216,12 @@ func (l *Linux) Init(data types.UnikernelParams) error { } l.configureNetwork(data.Net) + // rewrite the guest's resolv.conf to point at virtual resolver if needed. + if data.Net.DNSServer != "" && data.Net.ResolvConfPath != "" { + if err := writeResolvConf(data.Net.ResolvConfPath, data.Net.DNSServer); err != nil { + return fmt.Errorf("failed to configure guest DNS: %w", err) + } + } l.Blk = data.Block l.RootFsType = data.Rootfs.Type l.Env = data.EnvVars @@ -271,6 +278,32 @@ func (l *Linux) configureNetwork(net types.NetDevParams) { l.Net.Mask = net.Mask } +func writeResolvConf(path, resolvIP string) error { + data, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read %s: %w", path, err) + } + + lines := strings.Split(string(data), "\n") + out := make([]string, 0, len(lines)+1) + + // TODO: this assummes we have one nameserver that needs to rewrite + // which is the case for Docker. Could we need to preserve + // more than one nameserver in other cases ? + for _, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), "nameserver") { + out = append(out, "nameserver "+resolvIP) + continue + } + out = append(out, line) + } + + if err := os.WriteFile(path, []byte(strings.Join(out, "\n")), 0o644); err != nil { //nolint: gosec + return fmt.Errorf("failed to write %s: %w", path, err) + } + return nil +} + // setupUrunitConfig creates the urunit configuration file with environment variables. func (l *Linux) setupUrunitConfig(rfs types.RootfsParams) error { urunitConfig := l.buildUrunitConfig() diff --git a/pkg/unikontainers/unikontainers.go b/pkg/unikontainers/unikontainers.go index fc297f93..9ea2ddfe 100644 --- a/pkg/unikontainers/unikontainers.go +++ b/pkg/unikontainers/unikontainers.go @@ -30,6 +30,7 @@ import ( "syscall" "github.com/urunc-dev/urunc/pkg/network" + "github.com/urunc-dev/urunc/pkg/network/localhost" "github.com/urunc-dev/urunc/pkg/unikontainers/hypervisors" "github.com/urunc-dev/urunc/pkg/unikontainers/types" "github.com/urunc-dev/urunc/pkg/unikontainers/unikernels" @@ -199,7 +200,7 @@ func (u *Unikontainer) SetRunningState() error { return u.saveContainerState() } -func (u *Unikontainer) SetupNet() (types.NetDevParams, error) { +func (u *Unikontainer) SetupNet(fwd localhost.Forwarder) (types.NetDevParams, error) { networkType := u.getNetworkType() uniklog.WithField("network type", networkType).Debug("Retrieved network type") netArgs := types.NetDevParams{} @@ -207,8 +208,7 @@ func (u *Unikontainer) SetupNet() (types.NetDevParams, error) { if err != nil { return netArgs, fmt.Errorf("failed to create network manager for %s type: %v", networkType, err) } - - networkInfo, err := netManager.NetworkSetup(u.Spec.Process.User.UID, u.Spec.Process.User.GID) + networkInfo, err := netManager.NetworkSetup(u.Spec.Process.User.UID, u.Spec.Process.User.GID, fwd) if err != nil { // TODO: Handle this case better. We do not need to show an error // since there was no network in the container. Therefore, we @@ -226,6 +226,8 @@ func (u *Unikontainer) SetupNet() (types.NetDevParams, error) { // virtual ethernet interface inside the namespace netArgs.MAC = networkInfo.EthDevice.MAC netArgs.MTU = networkInfo.EthDevice.MTU + netArgs.DNSServer = networkInfo.DNSServer + netArgs.ResolvConfPath = networkInfo.ResolvConfPath } return netArgs, nil @@ -401,8 +403,15 @@ func (u *Unikontainer) Exec(metrics m.Writer) error { unikernelParams.CmdLine = strings.Fields(u.State.Annotations[annotCmdLine]) } + // Detect whether we need tap exposes services on the loopback that + // the guest needs reached. + fwd, err := localhost.DetectForwarder(u.State.ID, u.UruncCfg.Network.DNSResolverIP) + if err != nil { + uniklog.Errorf("failed to detect localhost forwarder: %v", err) + } + // handle network - netArgs, err := u.SetupNet() + netArgs, err := u.SetupNet(fwd) if err != nil { uniklog.Errorf("failed to setup network: %v", err) return err diff --git a/pkg/unikontainers/urunc_config.go b/pkg/unikontainers/urunc_config.go index 5f21d106..0d89dd0c 100644 --- a/pkg/unikontainers/urunc_config.go +++ b/pkg/unikontainers/urunc_config.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/BurntSushi/toml" + "github.com/urunc-dev/urunc/internal/constants" "github.com/urunc-dev/urunc/pkg/unikontainers/types" ) @@ -34,9 +35,14 @@ type UruncTimestamps struct { Destination string `toml:"destination"` // Used to specify a file for timestamps } +type UruncNetwork struct { + DNSResolverIP string `toml:"dns_resolver_ip"` +} + type UruncConfig struct { Log UruncLog `toml:"log"` Timestamps UruncTimestamps `toml:"timestamps"` + Network UruncNetwork `toml:"network"` Monitors map[string]types.MonitorConfig `toml:"monitors"` ExtraBins map[string]types.ExtraBinConfig `toml:"extra_binaries"` } @@ -78,6 +84,12 @@ func defaultTimestampsConfig() UruncTimestamps { } } +func defaultNetworkConfig() UruncNetwork { + return UruncNetwork{ + DNSResolverIP: constants.LocalhostDNSResolverIP, + } +} + func defaultMonitorsConfig() map[string]types.MonitorConfig { return map[string]types.MonitorConfig{ "qemu": {DefaultMemoryMB: 256, DefaultVCPUs: 1}, @@ -98,6 +110,7 @@ func defaultUruncConfig() *UruncConfig { return &UruncConfig{ Log: defaultLogConfig(), Timestamps: defaultTimestampsConfig(), + Network: defaultNetworkConfig(), Monitors: defaultMonitorsConfig(), ExtraBins: defaultExtraBinConfig(), } @@ -120,6 +133,9 @@ func (p *UruncConfig) Map() map[string]string { // them to this map. this map will be used to save the rest of the urunc config to state.json cfgMap := make(map[string]string) + if p.Network.DNSResolverIP != "" { + cfgMap["urunc_config.network.dns_resolver_ip"] = p.Network.DNSResolverIP + } for hv, hvCfg := range p.Monitors { prefix := "urunc_config.monitors." + hv + "." cfgMap[prefix+"default_memory_mb"] = strconv.FormatUint(uint64(hvCfg.DefaultMemoryMB), 10) @@ -140,10 +156,14 @@ func UruncConfigFromMap(cfgMap map[string]string) *UruncConfig { // since log and timestamps are loaded at the start of urunc, we will not be reading // them from this map. this map will be used to parse the rest of the urunc config from state.json cfg := &UruncConfig{ + Network: defaultNetworkConfig(), Monitors: defaultMonitorsConfig(), ExtraBins: defaultExtraBinConfig(), } + if val, ok := cfgMap["urunc_config.network.dns_resolver_ip"]; ok && val != "" { + cfg.Network.DNSResolverIP = val + } for key, val := range cfgMap { if !strings.HasPrefix(key, "urunc_config.monitors.") { continue diff --git a/pkg/unikontainers/urunc_config_test.go b/pkg/unikontainers/urunc_config_test.go index 89328847..9be63add 100644 --- a/pkg/unikontainers/urunc_config_test.go +++ b/pkg/unikontainers/urunc_config_test.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/urunc-dev/urunc/internal/constants" "github.com/urunc-dev/urunc/pkg/unikontainers/types" ) @@ -31,6 +32,7 @@ const ( testHvtMemoryKey = "urunc_config.monitors.hvt.default_memory_mb" testVirtiofsdPathKey = "urunc_config.extra_binaries.virtiofsd.path" testVirtiofsdOptsKey = "urunc_config.extra_binaries.virtiofsd.options" + testDNSResolverIPKey = "urunc_config.network.dns_resolver_ip" testVirtiofsdDefOpts = "--cache always --sandbox none" testBinOpts = "opt1 opt2" testQemuBinaryPath = "/usr/bin/qemu" @@ -48,6 +50,19 @@ func TestUruncConfigFromMap(t *testing.T) { assert.NotNil(t, config) assert.Equal(t, defaultMonitorsConfig(), config.Monitors) assert.Equal(t, defaultExtraBinConfig(), config.ExtraBins) + assert.Equal(t, defaultNetworkConfig(), config.Network) + }) + + t.Run("network dns resolver ip is picked up", func(t *testing.T) { + t.Parallel() + cfgMap := map[string]string{ + testDNSResolverIPKey: "192.168.150.150", + } + + config := UruncConfigFromMap(cfgMap) + + assert.NotNil(t, config) + assert.Equal(t, "192.168.150.150", config.Network.DNSResolverIP) }) t.Run("single monitor with all fields", func(t *testing.T) { @@ -381,6 +396,7 @@ func TestUruncConfigMap(t *testing.T) { "urunc_config.monitors.firecracker.binary_path", testVirtiofsdPathKey, testVirtiofsdOptsKey, + testDNSResolverIPKey, } for _, key := range expectedKeys { @@ -393,6 +409,7 @@ func TestUruncConfigMap(t *testing.T) { assert.Equal(t, "", cfgMap[testQemuBinaryKey]) assert.Equal(t, "/usr/libexec/virtiofsd", cfgMap[testVirtiofsdPathKey]) assert.Equal(t, testVirtiofsdDefOpts, cfgMap[testVirtiofsdOptsKey]) + assert.Equal(t, constants.LocalhostDNSResolverIP, cfgMap[testDNSResolverIPKey]) }) t.Run("custom config produces expected map", func(t *testing.T) {