Skip to content

feat: forward guest DNS to Docker's embedded resolver - #793

Open
alimx07 wants to merge 1 commit into
urunc-dev:mainfrom
alimx07:feat/add-docker-dns
Open

feat: forward guest DNS to Docker's embedded resolver#793
alimx07 wants to merge 1 commit into
urunc-dev:mainfrom
alimx07:feat/add-docker-dns

Conversation

@alimx07

@alimx07 alimx07 commented Jun 28, 2026

Copy link
Copy Markdown

Description

On Docker user custom networks the DNS resolver (127.0.0.11) is loopback only and unreachable by the unikernel guest. Detect this case per container, expose the resolver via a virtual resolver IP configured by user using tc redirects between the tap and lo , and rewrite the guest's resolv.conf to point at that IP in our unikernel is linux one. Also we add a custom rules DNAT rules according to the enviroment (e.g. Docker -> PREROUTING DNAT (for DNS server ports))

Two TC rules added on tap and lo:

  • Tap : Redirect packets to resolvIP into lo
  • Lo : Redirect packets with src=ResolvIP into Tap

Related issues

How was this tested?

Tested in user custom docker network with urunc container u1 and normal one u2:

u1 lookup on github.com

/ # nslookup github.com
Server:		172.24.255.254
Address:	172.24.255.254:53

Non-authoritative answer:
Name:	github.com
Address: 140.82.121.3

u1 lookup on u2

/ # nslookup u2.
Server:		172.24.255.254
Address:	172.24.255.254:53

Non-authoritative answer:

Non-authoritative answer:
Name:	u2
Address: 172.24.0.3

u2 lookup on u1

Server:		127.0.0.11
Address:	127.0.0.11:53

Non-authoritative answer:

Non-authoritative answer:
Name:	u1
Address: 172.24.0.2

LLM usage

Checklist

  • I have read the contribution guide.
  • The linter passes locally (make lint).
  • The e2e tests of at least one tool pass locally (make test_ctr, make test_nerdctl, make test_docker, make test_crictl).
  • If LLMs were used: I have read the llm policy.

@netlify

netlify Bot commented Jun 28, 2026

Copy link
Copy Markdown

Deploy Preview for urunc canceled.

Name Link
🔨 Latest commit f278a84
🔍 Latest deploy log https://app.netlify.com/projects/urunc/deploys/6a6f2a3780ab980008becbf0

@alimx07
alimx07 force-pushed the feat/add-docker-dns branch 2 times, most recently from 25a6350 to a743f29 Compare June 28, 2026 07:44
@alimx07
alimx07 force-pushed the feat/add-docker-dns branch from a743f29 to 6723c67 Compare July 10, 2026 15:49
@alimx07

alimx07 commented Jul 10, 2026

Copy link
Copy Markdown
Author

Hey @cmainas, I have implemented the changes we discussed during our sync. Could you please take a look and review them? This will allow us to make any necessary changes before we extend this logic to other unikernels.

@alimx07
alimx07 force-pushed the feat/add-docker-dns branch 6 times, most recently from 22a5784 to 89b24e6 Compare July 23, 2026 14:29

@cmainas cmainas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @alimx07 ,

thank you for all the changes and the fix. A few comments:

  • In the e2e testing the docker network does not exist and therefore creating the contianer will fail. We need to setup the network. Maybe in https://github.com/urunc-dev/urunc/blob/main/tests/e2e/docker_test.go#L25 and also remove later.
  • Let;s find a better name for loclahost.go. Maybe something like "dns_at_localhost.go"?
  • I think I got confused with the genericRules and dockerRules. Have we found a case where we should apply the genericRules (e.g. a CNI) rather than dockerRules?
  • We can wrap the execution of iptables in a function, instead of having cmd and cmd.Run in various places.
  • We should check if we can obtain the information we gather form iptables from other sources (e.g. netlink).
  • We can restructure the code and this logic in the network_dynamic.go (the only mode where these changes apply) and avoid re-opening the tap device etc. We can me use of https://github.com/urunc-dev/urunc/blob/main/pkg/network/network_dynamic.go#L23C6-L23C20 for passing information.
  • We should also clean up all the rules we apply.

Comment thread tests/e2e/test_cases.go Outdated
Comment thread pkg/network/localhost/localhost.go Outdated
Comment thread pkg/unikontainers/unikontainers.go Outdated
Comment thread pkg/network/localhost/localhost.go Outdated
Comment thread pkg/network/localhost/localhost.go Outdated
Comment thread pkg/network/localhost/docker.go
@alimx07
alimx07 force-pushed the feat/add-docker-dns branch 2 times, most recently from 39be2e9 to 7e73541 Compare July 24, 2026 17:34
info.DNSServer = n.DNSForwarder.VirtIP.String()
netlog.Debugf("localhost forwarding applied: virtual resolvIP %s", info.DNSServer)
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since users are already familiar with the urunc localhost resolver issue, making this a hard failure is probably the wrong move. I'll fail open here for now. We can always flag it later if needed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my comment and the original code have disappeared, so I do not really know what we are talking about here. However, it is ok not to fail hard. We cna easily change that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One note though. If RewriteResolvConf fails and Apply succeeds, we will end up with some network setup which might create issues. So, for every failure step, we need to perform the necessary cleanup. This can be addressed in a future PR, but we should at least document this with a TODO.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added it as TODO now, I will look into add after adding more unikernels :)

Comment thread tests/e2e/common.go
_, err := commonCmdExec(cmdBase)
return err
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cmainas , Regarding network. I think using the same pattern as container (rm , create) is the cleanest way here.
I know this is out of the scope of the PR now, and it just being larger and larger, but what touched here is minimal as possible. Also , a future PR will be opened (by Me) adding more test cases (instead of docker) touching other CLI tools. so if you are good, we can move with this right now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is ok for the time being, let's just make sure we record it so we can fix it later.

Comment thread pkg/network/localhost/docker.go

@cmainas cmainas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @alimx07 ,

thank you for the changes:

  • The testing is ok (for the time being) to follow the container create flow. We can properly fix that in later iteration.

info.DNSServer = n.DNSForwarder.VirtIP.String()
netlog.Debugf("localhost forwarding applied: virtual resolvIP %s", info.DNSServer)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my comment and the original code have disappeared, so I do not really know what we are talking about here. However, it is ok not to fail hard. We cna easily change that.

Comment thread tests/e2e/suite_test.go
Comment thread tests/e2e/common.go
_, err := commonCmdExec(cmdBase)
return err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is ok for the time being, let's just make sure we record it so we can fix it later.

Comment thread pkg/network/localhost/docker.go
// ClearRules removes DNAT rules dockerRules could have added.
func clearRules() error {

// This will be run from kill() so no memory state could be used.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not need to keep any memory, we just need to remove the rules we applied before. Is there something that restricts us form doing so?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I am just write a comment as reasoning for future reader why we do not just store iptables from create command and delete them later instead of query and detect them again.

Comment thread pkg/network/localhost/docker.go Outdated
return "", "", fmt.Errorf("could not list TCP sockets in namespace: %w", err)
}
for _, s := range tcp {
if s.InetDiagMsg.ID.Source.String() == loIP.String() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make this check more precise because there might be other open TCP sockets here and we might end up with the wrong port. We should also check the state of the socket to be a listener.

Comment thread pkg/network/localhost/docker.go Outdated
return "", "", fmt.Errorf("could not list UDP sockets in namespace: %w", err)
}
for _, s := range udp {
if s.InetDiagMsg.ID.Source.String() == loIP.String() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly with the previous comment, we have to make this check more precise too. Looking it up, one idea is to see of the destination is "0.0.0.0".

info.DNSServer = n.DNSForwarder.VirtIP.String()
netlog.Debugf("localhost forwarding applied: virtual resolvIP %s", info.DNSServer)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One note though. If RewriteResolvConf fails and Apply succeeds, we will end up with some network setup which might create issues. So, for every failure step, we need to perform the necessary cleanup. This can be addressed in a future PR, but we should at least document this with a TODO.

fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "nameserver" {
if ip := net.ParseIP(fields[1]); ip != nil && ip.IsLoopback() {
return ip

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop here is correct, but there might be cases where the /etc/resolv/conf file has more than one nameserver entries. So we should check if there is just a single nameserver with the loopback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay you are right, lets be determinstic about this and to give the context and visuallize it.

type Forwarder struct {
	VirtIP     net.IP // virtual resolver IP the guest dials
	LoIP       net.IP // loopback IP the real resolver listens on
	ResolvConf string // host path of the container's resolv.conf
	custom     rules  // scenario-specific extra rules, picked by Detect
}


// loNameserver returns the first loopback nameserver in resolv.conf data, if any.
func loNameserver(data string) net.IP {
	for line := range strings.SplitSeq(data, "\n") {
		fields := strings.Fields(line)
		if len(fields) >= 2 && fields[0] == "nameserver" {
			if ip := net.ParseIP(fields[1]); ip != nil && ip.IsLoopback() {
				return ip
			}
		}
	}
	return nil
}

if we have resolv.conf as

nameserver 127.0.0.10
nameserver 127.0.0.20
nameserver 8.8.8.8

our loNameserver function above will detect and set our loIP as 127.0.0.10 (first localhost nameserver), which will be the one in TC rules and Iptables one. Knowing that we should only rewrite the first nameserver in our resolv.conf, so final result will be:

nameserver virtIP
nameserver 127.0.0.20
nameserver 8.8.8.8

I know that a pushback here is to rewrite all localhost nameservers , but this will lead us to also write more iptables rules and TC rules, depending only on the first localhost nameserver is acceptable I think.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, but I meant something else. In the file format you mentioned

nameserver 127.0.0.10
nameserver 127.0.0.20
nameserver 8.8.8.8

if the first two entries fail, then 8.8.8.8 will be used and it will work (for some dns resolutions). So what I meant is we target specifically the docker case with a single nameserver at a localhost address (at least for this PR). In the future we can extend the current implementation and address the above scenario too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the current implementation works for docker case, we can just leave it like that. If I understood correctly from docker, there will be no case with multiple nameserver entries in user defined networks.

@cmainas

cmainas commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Also, one more thing, do not forget to add yourself in https://github.com/urunc-dev/urunc/blob/main/.github/contributors.yaml

return nil
}

func uint16Ptr(v uint16) *uint16 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can not we inline this?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have tried but it fails everytime.

@cmainas cmainas Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does fail mean? Is it a compiler error, a runtime error, other failure?

@alimx07
alimx07 force-pushed the feat/add-docker-dns branch from 7e73541 to b27f329 Compare July 30, 2026 01:14
@alimx07

alimx07 commented Jul 30, 2026

Copy link
Copy Markdown
Author

@cmainas . Everything almost done. Let me if you have any other comments. meanwhile I will be working on sideContainers case for now :)

@cmainas

cmainas commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Hello @alimx07 , please rebase over the main branch.

@alimx07
alimx07 force-pushed the feat/add-docker-dns branch from b27f329 to d77880e Compare July 30, 2026 12:24
@alimx07

alimx07 commented Jul 30, 2026

Copy link
Copy Markdown
Author

Hello @alimx07 , please rebase over the main branch.

Hello @cmainas , Done

@cmainas cmainas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @alimx07 , some comments:

Comment thread go.mod Outdated
Comment on lines +33 to +36
golang.org/x/net v0.56.0
golang.org/x/sys v0.46.0
google.golang.org/grpc v1.81.1
k8s.io/cri-api v0.36.2

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have these changed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just random go mod tidy. No problems I revert it now and rebased also :)

Comment thread pkg/network/localhost/local_dns.go Outdated
// and set our `loIP` as `127.0.0.10` (first loopback nameserver),
// which will be the one in TC rules and Iptables one.
// Knowing that we should only rewrite the first nameserver in our `resolv.conf`,
// so final result will be :

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: space before ":"

Comment thread pkg/network/localhost/local_dns.go Outdated
for _, line := range lines {
fields := strings.Fields(line)

// if we have `resolv.conf` as :

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not: space before ":"

@alimx07
alimx07 force-pushed the feat/add-docker-dns branch from c27ecf5 to 9220678 Compare August 2, 2026 11:25
Detect first loopback nameserver in our resolv.conf and rewrite
to user defined virtIP (configured in urunc constants) and apply some
TC rules in general and IP rules (dockercase) so we can route this
DNS quiries to host localhost DNS server.

Signed-off-by: Ali Mohamed <amx746@gmail.com>
@alimx07
alimx07 force-pushed the feat/add-docker-dns branch from 9220678 to f278a84 Compare August 2, 2026 11:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants