Repository: srl-labs/containerlab
Pull request #1650 (2023-10-16 10:05:21)

Query hosts dns servers and populate nodes config with it
Fixing #1638 

steiler: I'm not too happy about calling `PrepareDNSServers()` in `deployFn()`. @hellt opinion?
steiler: An option might be to do that stuff as part of https://github.com/srl-labs/containerlab/blob/main/types/topology.go#L508

We maybe add the `ExtractDNSServerFromResolvConf()` to the topology, cache the result and in the GetDNS function we evaluate if DNS servers are set, if not, we populate it with the determined system DNS IPs
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) Report
> Merging [#1650](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (f47266d) into [main](https://app.codecov.io/gh/srl-labs/containerlab/commit/f3d255345d18ecc0d3c05a58d16f1435ba5800df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (f3d2553) will **increase** coverage by `0.08%`.
> Report is 1 commits behind head on main.
> The diff coverage is `74.13%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1650      +/-   ##
==========================================
+ Coverage   50.88%   50.96%   +0.08%     
==========================================
  Files         135      136       +1     
  Lines       13107    13159      +52     
==========================================
+ Hits         6669     6706      +37     
- Misses       5706     5716      +10     
- Partials      732      737       +5     
```

| [Files](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Coverage Δ | |
|---|---|---|
| [nodes/srl/srl.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvc3JsL3NybC5nbw==) | `56.75% <100.00%> (+0.09%)` | :arrow_up: |
| [cmd/deploy.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL2RlcGxveS5nbw==) | `65.05% <0.00%> (-0.69%)` | :arrow_down: |
| [clab/clab.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jbGFiLmdv) | `76.62% <66.66%> (-0.37%)` | :arrow_down: |
| [utils/resolve.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvcmVzb2x2ZS5nbw==) | `80.00% <80.00%> (ø)` | |

</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) Report
> Merging [#1650](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (f47266d) into [main](https://app.codecov.io/gh/srl-labs/containerlab/commit/f3d255345d18ecc0d3c05a58d16f1435ba5800df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (f3d2553) will **increase** coverage by `0.08%`.
> Report is 1 commits behind head on main.
> The diff coverage is `74.13%`.

> :exclamation: Current head f47266d differs from pull request most recent head 5fc4c50. Consider uploading reports for the commit 5fc4c50 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1650      +/-   ##
==========================================
+ Coverage   50.88%   50.96%   +0.08%     
==========================================
  Files         135      136       +1     
  Lines       13107    13159      +52     
==========================================
+ Hits         6669     6706      +37     
- Misses       5706     5716      +10     
- Partials      732      737       +5     
```

| [Files](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Coverage Δ | |
|---|---|---|
| [nodes/srl/srl.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvc3JsL3NybC5nbw==) | `56.75% <100.00%> (+0.09%)` | :arrow_up: |
| [cmd/deploy.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL2RlcGxveS5nbw==) | `65.05% <0.00%> (-0.69%)` | :arrow_down: |
| [clab/clab.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jbGFiLmdv) | `76.62% <66.66%> (-0.37%)` | :arrow_down: |
| [utils/resolve.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvcmVzb2x2ZS5nbw==) | `80.00% <80.00%> (ø)` | |

</details>
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) Report
> Merging [#1650](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (5fc4c50) into [main](https://app.codecov.io/gh/srl-labs/containerlab/commit/f3d255345d18ecc0d3c05a58d16f1435ba5800df?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) (f3d2553) will **increase** coverage by `0.08%`.
> Report is 1 commits behind head on main.
> The diff coverage is `74.13%`.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1650      +/-   ##
==========================================
+ Coverage   50.88%   50.96%   +0.08%     
==========================================
  Files         135      136       +1     
  Lines       13107    13159      +52     
==========================================
+ Hits         6669     6706      +37     
- Misses       5706     5716      +10     
- Partials      732      737       +5     
```

| [Files](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs) | Coverage Δ | |
|---|---|---|
| [nodes/srl/srl.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-bm9kZXMvc3JsL3NybC5nbw==) | `56.75% <100.00%> (+0.09%)` | :arrow_up: |
| [cmd/deploy.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y21kL2RlcGxveS5nbw==) | `65.05% <0.00%> (-0.69%)` | :arrow_down: |
| [clab/clab.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-Y2xhYi9jbGFiLmdv) | `76.62% <66.66%> (-0.37%)` | :arrow_down: |
| [utils/resolve.go](https://app.codecov.io/gh/srl-labs/containerlab/pull/1650?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=srl-labs#diff-dXRpbHMvcmVzb2x2ZS5nbw==) | `80.00% <80.00%> (ø)` | |

</details>

Patch:
diff --git a/clab/clab.go b/clab/clab.go
index 500e38ae..cad0488b 100644
--- a/clab/clab.go
+++ b/clab/clab.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"io/fs"
 	"os"
 	"path/filepath"
 	"strings"
@@ -25,6 +26,7 @@ import (
 	"github.com/srl-labs/containerlab/runtime/docker"
 	"github.com/srl-labs/containerlab/runtime/ignite"
 	"github.com/srl-labs/containerlab/types"
+	"github.com/srl-labs/containerlab/utils"
 	"golang.org/x/crypto/ssh"
 	"golang.org/x/exp/slices"
 )
@@ -283,6 +285,14 @@ func NewContainerLab(opts ...ClabOption) (*CLab, error) {
 	if c.TopoPaths.TopologyFileIsSet() {
 		err = c.parseTopology()
 	}
+
+	// Extract the host systems DNS servers and populate the
+	// Nodes DNS Config with these if not specifically provided
+	fileSystem := os.DirFS("/")
+	if err := c.ExtractDNSServers(fileSystem); err != nil {
+		return nil, err
+	}
+
 	return c, err
 }
 
@@ -760,3 +770,34 @@ func (c *CLab) ResolveLinks() error {
 
 	return nil
 }
+
+// ExtractDNSServers extracts DNS servers from the resolv.conf files
+// and populates the Nodes DNS Config with these if not specifically provided.
+func (c *CLab) ExtractDNSServers(filesys fs.FS) error {
+	// extract DNS servers from the relevant resolv.conf files
+	DNSServers, err := utils.ExtractDNSServersFromResolvConf(filesys,
+		[]string{"etc/resolv.conf", "run/systemd/resolve/resolv.conf"})
+	if err != nil {
+		return err
+	}
+
+	// no DNS Servers found, return
+	if len(DNSServers) == 0 {
+		return nil
+	}
+
+	// if no dns servers are explicitly configured,
+	// we set the DNS servers that we've extracted.
+	for _, n := range c.Nodes {
+		config := n.Config()
+		if config.DNS == nil {
+			config.DNS = &types.DNSConfig{}
+		}
+
+		if n.Config().DNS.Servers == nil {
+			n.Config().DNS.Servers = DNSServers
+		}
+	}
+
+	return nil
+}
diff --git a/docs/manual/kinds/srl.md b/docs/manual/kinds/srl.md
index c60b1bf0..a20488b2 100644
--- a/docs/manual/kinds/srl.md
+++ b/docs/manual/kinds/srl.md
@@ -349,7 +349,7 @@ banner  cli  config.json  devices  tls  ztp
 
 The topology file that defines the emulated hardware type is driven by the value of the kinds `type` parameter. Depending on a specified `type`, the appropriate content will be populated into the `topology.yml` file that will get mounted to `/tmp/topology.yml` directory inside the container in `ro` mode.
 
-#### authorized keys
+#### Authorized keys
 
 Additionally, containerlab will mount the `authorized_keys` file that will have contents of every public key found in `~/.ssh` directory as well as the contents of a `~/.ssh/authorized_keys` file if it exists[^2]. This file will be mounted to `~/.ssh/authorized_keys` path for the following users:
 
@@ -359,6 +359,35 @@ Additionally, containerlab will mount the `authorized_keys` file that will have
 
 This will enable passwordless access for the users above if any public key is found in the user's directory.
 
+#### DNS configuration
+
+SR Linux's management stack lives in a separate network namespace `srbase-mgmt`. Due to this fact, the DNS resolver provided by Docker in the root network namespace is not available to the SR Linux management stack.
+
+To enable DNS resolution for SR Linux, containerlab will extract the DNS servers configured on the host system from
+
+* `/etc/resolv.conf`
+* `run/systemd/resolve/resolv.conf`
+
+files and configure IP addresses found there as DNS servers in the management network instance of SR Linux:
+
+```srl
+--{ running }--[  ]--
+A:srl# info system dns  
+    system {
+        dns {
+            network-instance mgmt
+            server-list [
+                # these servers were extracted from the host
+                # and provisioned by containerlab
+                10.171.10.1
+                10.171.10.2
+            ]
+        }
+    }
+```
+
+If you wish to turn off the automatic DNS provisioning, set the `servers` list to an empty value in the [node configuration](../nodes.md#dns).
+
 ## Host Requirements
 
 SR Linux is a containerized NOS, therefore it depends on the host's kernel and CPU. It is recommended to run a kernel v4 and newer, though it might also run on the older kernels.
diff --git a/go.mod b/go.mod
index 88bec24d..84c43cae 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ require (
 	github.com/awalterschulze/gographviz v2.0.3+incompatible
 	github.com/containernetworking/plugins v1.3.0
 	github.com/containers/common v0.56.0
-	github.com/containers/podman/v4 v4.7.0
+	github.com/containers/podman/v4 v4.7.1
 	github.com/digitalocean/go-openvswitch v0.0.0-20201214180534-ce0f183468d8
 	github.com/distribution/reference v0.5.0
 	github.com/docker/docker v24.0.6+incompatible
@@ -15,7 +15,7 @@ require (
 	github.com/docker/go-units v0.5.0
 	github.com/dustin/go-humanize v1.0.1
 	github.com/florianl/go-tc v0.4.2
-	github.com/google/go-cmp v0.5.9
+	github.com/google/go-cmp v0.6.0
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
 	github.com/google/uuid v1.3.1
 	github.com/h2non/gock v1.2.0
@@ -274,7 +274,7 @@ require (
 	go4.org/unsafe/assume-no-moving-gc v0.0.0-20230204201903-c31fa085b70e // indirect
 	gocloud.dev v0.30.0 // indirect
 	golang.org/x/mod v0.13.0
-	golang.org/x/net v0.16.0
+	golang.org/x/net v0.17.0
 	golang.org/x/oauth2 v0.12.0 // indirect
 	golang.org/x/sync v0.3.0 // indirect
 	golang.org/x/text v0.13.0 // indirect
diff --git a/go.sum b/go.sum
index ab2119bb..def0b90f 100644
--- a/go.sum
+++ b/go.sum
@@ -1122,8 +1122,8 @@ github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B
 github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=
 github.com/containers/ocicrypt v1.1.8 h1:saSBF0/8DyPUjzcxMVzL2OBUWCkvRvqIm75pu0ADSZk=
 github.com/containers/ocicrypt v1.1.8/go.mod h1:jM362hyBtbwLMWzXQZTlkjKGAQf/BN/LFMtH0FIRt34=
-github.com/containers/podman/v4 v4.7.0 h1:KjawmmazJHyQfck5sFLzQ/KI+JtsnnoGI+d+qfiPvaY=
-github.com/containers/podman/v4 v4.7.0/go.mod h1:FJPqIhiwdklJenJskZyoNd1ZNin6kvY6zL9Rypaajxs=
+github.com/containers/podman/v4 v4.7.1 h1:tMhVDvvCIMU64UZVSo2EfLG7ff0/GDHkDg7An2rJ0ZA=
+github.com/containers/podman/v4 v4.7.1/go.mod h1:FJPqIhiwdklJenJskZyoNd1ZNin6kvY6zL9Rypaajxs=
 github.com/containers/psgo v1.8.0 h1:2loGekmGAxM9ir5OsXWEfGwFxorMPYnc6gEDsGFQvhY=
 github.com/containers/psgo v1.8.0/go.mod h1:T8ZxnX3Ur4RvnhxFJ7t8xJ1F48RhiZB4rSrOaR/qGHc=
 github.com/containers/storage v1.43.0/go.mod h1:uZ147thiIFGdVTjMmIw19knttQnUCl3y9zjreHrg11s=
@@ -1683,8 +1683,9 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
 github.com/google/go-containerregistry v0.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ=
 github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ=
@@ -3193,8 +3194,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
-golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
-golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
diff --git a/nodes/srl/srl.go b/nodes/srl/srl.go
index 07281ac6..8a29a05c 100644
--- a/nodes/srl/srl.go
+++ b/nodes/srl/srl.go
@@ -59,6 +59,10 @@ set / system gnmi-server admin-state enable network-instance mgmt admin-state en
 set / system gnmi-server rate-limit 65000
 set / system gnmi-server trace-options [ request response common ]
 set / system gnmi-server unix-socket admin-state enable
+{{- if .DNSServers }}
+set / system dns network-instance mgmt
+set / system dns server-list [ {{ range $dnsserver := .DNSServers}}{{$dnsserver}} {{ end }}]
+{{- end }}
 set / system json-rpc-server admin-state enable network-instance mgmt http admin-state enable
 set / system json-rpc-server admin-state enable network-instance mgmt https admin-state enable tls-profile clab-profile
 set / system snmp community public
@@ -553,6 +557,7 @@ type srlTemplateData struct {
 	IFaces     map[string]tplIFace
 	SSHPubKeys string
 	MgmtMTU    int
+	DNSServers []string
 }
 
 // tplIFace template interface struct.
@@ -573,12 +578,13 @@ func (n *srl) addDefaultConfig(ctx context.Context) error {
 	// struct that holds data used in templating of the default config snippet
 
 	tplData := srlTemplateData{
-		TLSKey:    n.Cfg.TLSKey,
-		TLSCert:   n.Cfg.TLSCert,
-		TLSAnchor: n.Cfg.TLSAnchor,
-		Banner:    b,
-		IFaces:    map[string]tplIFace{},
-		MgmtMTU:   0,
+		TLSKey:     n.Cfg.TLSKey,
+		TLSCert:    n.Cfg.TLSCert,
+		TLSAnchor:  n.Cfg.TLSAnchor,
+		Banner:     b,
+		IFaces:     map[string]tplIFace{},
+		MgmtMTU:    0,
+		DNSServers: n.Config().DNS.Servers,
 	}
 
 	n.filterSSHPubKeys()
diff --git a/utils/networkcli.go b/utils/networkcli.go
index 3d3a15d4..94309896 100644
--- a/utils/networkcli.go
+++ b/utils/networkcli.go
@@ -26,6 +26,10 @@ var (
 			"exec": "docker",
 			"open": "exec -it",
 		},
+		"podman": {
+			"exec": "podman",
+			"open": "exec -it",
+		},
 	}
 )
 
diff --git a/utils/resolve.go b/utils/resolve.go
new file mode 100644
index 00000000..25cbe9c9
--- /dev/null
+++ b/utils/resolve.go
@@ -0,0 +1,58 @@
+package utils
+
+import (
+	"bufio"
+	"io/fs"
+	"net"
+	"strings"
+
+	log "github.com/sirupsen/logrus"
+)
+
+// ExtractDNSServersFromResolvConf extracts IP addresses
+// of the DNS servers from the resolv.conf-formatted files passed in filenames list.
+// Returns a list of IP addresses of the DNS servers.
+func ExtractDNSServersFromResolvConf(filesys fs.FS, filenames []string) ([]string, error) {
+	DNSServersMap := map[string]struct{}{}
+
+	for _, filename := range filenames {
+		readFile, err := filesys.Open(filename)
+		if err != nil {
+			log.Debugf("Error opening host DNS config %s: %v", filename, err)
+			continue
+		}
+
+		fileScanner := bufio.NewScanner(readFile)
+		fileScanner.Split(bufio.ScanLines)
+
+		// check line by line for a match
+		for fileScanner.Scan() {
+			line := strings.TrimSpace(fileScanner.Text())
+			if strings.HasPrefix(line, "nameserver") {
+				fields := strings.Fields(line)
+				if len(fields) != 2 {
+					continue
+				}
+
+				ip := net.ParseIP(fields[1])
+				if ip == nil || ip.IsLoopback() {
+					continue
+				}
+
+				DNSServersMap[ip.String()] = struct{}{}
+			}
+		}
+
+		readFile.Close()
+	}
+
+	if len(DNSServersMap) == 0 {
+		return nil, nil
+	}
+
+	DNSServers := make([]string, 0, len(DNSServersMap))
+	for k := range DNSServersMap {
+		DNSServers = append(DNSServers, k)
+	}
+	return DNSServers, nil
+}


Test patch:
diff --git a/utils/resolve_test.go b/utils/resolve_test.go
new file mode 100644
index 00000000..8fcb6c30
--- /dev/null
+++ b/utils/resolve_test.go
@@ -0,0 +1,149 @@
+package utils
+
+import (
+	"io/fs"
+	"strings"
+	"testing"
+	"testing/fstest"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+)
+
+func TestExtractDNSServersFromResolvConf(t *testing.T) {
+	type args struct {
+		filesys   fs.FS
+		filenames []string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    []string
+		wantErr bool
+	}{
+		{
+			name: "One file local dns empty result",
+			args: args{
+				filesys: fstest.MapFS{
+					"etc/resolv.conf": &fstest.MapFile{
+						Data: []byte(
+							`
+# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
+# Do not edit.
+#
+# This file might be symlinked as /etc/resolv.conf. If you're looking at
+# /etc/resolv.conf and seeing this text, you have followed the symlink.
+
+nameserver 127.0.0.53
+options edns0 trust-ad
+search .					
+`,
+						),
+					},
+				},
+				filenames: []string{"etc/resolv.conf"},
+			},
+			want:    nil,
+			wantErr: false,
+		},
+		{
+			name: "Two files local dns and two remote, two results",
+			args: args{
+				filesys: fstest.MapFS{
+					"etc/resolv.conf": &fstest.MapFile{
+						Data: []byte(
+							`
+# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
+# Do not edit.
+#
+# This file might be symlinked as /etc/resolv.conf. If you're looking at
+# /etc/resolv.conf and seeing this text, you have followed the symlink.
+
+nameserver 1.1.1.1
+options edns0 trust-ad
+search .					
+`,
+						),
+					},
+					"etc/someother/resolv.conf": &fstest.MapFile{
+						Data: []byte(
+							`
+# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
+# Do not edit.
+
+nameserver 127.0.0.53
+nameserver 8.8.8.8
+options edns0 trust-ad
+search .					
+`,
+						),
+					},
+				},
+				filenames: []string{"etc/resolv.conf", "etc/someother/resolv.conf"},
+			},
+			want:    []string{"1.1.1.1", "8.8.8.8"},
+			wantErr: false,
+		},
+		{
+			name: "Duplicat 8.8.8.8",
+			args: args{
+				filesys: fstest.MapFS{
+					"etc/resolv.conf": &fstest.MapFile{
+						Data: []byte(
+							`
+# Do not edit.
+nameserver 1.1.1.1
+nameserver 8.8.8.8
+
+options edns0 trust-ad
+search .					
+`,
+						),
+					},
+					"etc/someother/resolv.conf": &fstest.MapFile{
+						Data: []byte(
+							`
+nameserver 8.8.8.8
+options edns0 trust-ad
+search .					
+`,
+						),
+					},
+				},
+				filenames: []string{"etc/resolv.conf", "etc/someother/resolv.conf"},
+			},
+			want:    []string{"1.1.1.1", "8.8.8.8"},
+			wantErr: false,
+		},
+		{
+			name: "Files do not exist",
+			args: args{
+				filesys:   fstest.MapFS{},
+				filenames: []string{"etc/resolv.conf", "etc/someother/resolv.conf"},
+			},
+			want:    nil,
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := ExtractDNSServersFromResolvConf(tt.args.filesys, tt.args.filenames)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("ExtractDNSServerFromResolvConf() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if diff := cmp.Diff(tt.want, got, cmpopts.SortSlices(func(s1, s2 string) bool {
+				switch strings.Compare(s1, s2) {
+				case -1:
+					return false
+				case 0:
+					return true
+				}
+				return true
+			},
+			)); diff != "" {
+				t.Errorf(diff)
+			}
+		})
+	}
+}