Skip to content

Commit a2440a9

Browse files
committed
feat(network): forward guest DNS to Docker's embedded resolver
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 (docker case) so we can route this DNS queries to host localhost DNS server. Signed-off-by: Ali Mohamed <amx746@gmail.com>
1 parent 005c4ba commit a2440a9

23 files changed

Lines changed: 810 additions & 25 deletions

.github/contributors.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,6 @@ users:
128128
OdysseasKalaitsidis:
129129
name: Odysseas Kalaitsidis
130130
email: odysseaskalaitsides@gmail.com
131+
alimx07:
132+
name: Ali Mohamed
133+
email: amx746@gmail.com

.github/linters/urunc-dict.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,3 +429,15 @@ hyperlight
429429
Hyperlight
430430
Odysseas
431431
Kalaitsidis
432+
lhlog
433+
dnat
434+
PREROUTING
435+
resolv
436+
ndots
437+
clsact
438+
Sysctls
439+
localnet
440+
skbedit
441+
ptype
442+
pedit
443+
nslookup

Makefile

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ URUNC_SRC += $(wildcard $(CURDIR)/pkg/unikontainers/unikernels/*.go)
7171
URUNC_SRC += $(wildcard $(CURDIR)/pkg/unikontainers/types/*.go)
7272
URUNC_SRC += $(wildcard $(CURDIR)/pkg/unikontainers/initrd/*.go)
7373
URUNC_SRC += $(wildcard $(CURDIR)/pkg/network/*.go)
74+
URUNC_SRC += $(wildcard $(CURDIR)/pkg/network/localhost/*.go)
7475
SHIM_SRC := $(wildcard $(CURDIR)/cmd/containerd-shim-urunc-v2/*.go)
7576
SHIM_SRC += $(wildcard $(CURDIR)/pkg/containerd-shim/*.go)
7677
SHIM_SRC += $(wildcard $(CURDIR)/pkg/containerd-shim/containerd/*.go)
@@ -233,7 +234,7 @@ test: unittest e2etest
233234

234235
## unittest Run all unit tests
235236
.PHONY: unittest
236-
unittest: test_unikontainers test_metrics test_network test_hypervisors test_unikernels
237+
unittest: test_unikontainers test_metrics test_network test_localhost test_hypervisors test_unikernels
237238

238239
## e2etest Run all end-to-end tests
239240
.PHONY: e2etest
@@ -257,6 +258,12 @@ test_network:
257258
@GOFLAGS=$(TEST_FLAGS) $(GO) test $(TEST_OPTS) ./pkg/network -v
258259
@echo " "
259260

261+
## test_localhost Run unit tests for network/localhost package
262+
test_localhost:
263+
@echo "Unit testing in pkg/network/localhost"
264+
@GOFLAGS=$(TEST_FLAGS) $(GO) test $(TEST_OPTS) ./pkg/network/localhost -v
265+
@echo " "
266+
260267
## test_hypervisors Run unit tests for hypervisors package
261268
test_hypervisors:
262269
@echo "Unit testing in hypervisors"

internal/constants/network_constants.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
StaticNetworkTapIP = "172.16.1.1"
1919
StaticNetworkUnikernelIP = "172.16.1.2"
2020
// TODO: Experiment with DynamicNetworkTapIP starting from 172.16.X.1
21-
DynamicNetworkTapIP = "172.16.X.2"
22-
QueueProxyRedirectIP = "172.16.1.2"
21+
DynamicNetworkTapIP = "172.16.X.2"
22+
QueueProxyRedirectIP = "172.16.1.2"
23+
LocalhostDNSResolverIP = "192.168.100.100"
2324
)

pkg/network/localhost/docker.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright (c) 2023-2026, Nubificus LTD
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package localhost
16+
17+
import (
18+
"errors"
19+
"fmt"
20+
"net"
21+
"os/exec"
22+
"strings"
23+
"syscall"
24+
25+
"github.com/vishvananda/netlink"
26+
)
27+
28+
func dockerRules(f *Forwarder) error {
29+
30+
tcpPort, udpPort, err := dockerDNSPorts(f.LoIP)
31+
if err != nil {
32+
return err
33+
}
34+
lhlog.Debugf("Docker DNS resolver: tcp/%s udp/%s", tcpPort, udpPort)
35+
36+
if err := dnat(f.VirtIP, "udp", net.JoinHostPort(f.LoIP.String(), udpPort)); err != nil {
37+
return err
38+
}
39+
if err := dnat(f.VirtIP, "tcp", net.JoinHostPort(f.LoIP.String(), tcpPort)); err != nil {
40+
return err
41+
}
42+
lhlog.Debug("Applied Docker DNAT rules")
43+
44+
return nil
45+
}
46+
47+
func dockerDNSPorts(loIP net.IP) (string, string, error) {
48+
var tcpPort, udpPort string
49+
50+
// syscall.AF_INET : IPv4
51+
tcp, err := netlink.SocketDiagTCPInfo(syscall.AF_INET)
52+
if err != nil {
53+
return "", "", fmt.Errorf("could not list TCP sockets in namespace: %w", err)
54+
}
55+
for _, s := range tcp {
56+
if s.InetDiagMsg.ID.Source.String() == loIP.String() && s.InetDiagMsg.ID.Destination.String() == "0.0.0.0" && int(s.InetDiagMsg.State) == netlink.TCP_LISTEN {
57+
tcpPort = fmt.Sprintf("%d", s.InetDiagMsg.ID.SourcePort)
58+
break
59+
}
60+
}
61+
udp, err := netlink.SocketDiagUDPInfo(syscall.AF_INET)
62+
if err != nil {
63+
return "", "", fmt.Errorf("could not list UDP sockets in namespace: %w", err)
64+
}
65+
for _, s := range udp {
66+
67+
// As udp has no connection states as tcp. by default (which is our case) it should have tcp_close (7) state to read it as "no peer association"
68+
// TODO: is there any other states that could make this condition fails ?
69+
if s.InetDiagMsg.ID.Source.String() == loIP.String() && s.InetDiagMsg.ID.Destination.String() == "0.0.0.0" && int(s.InetDiagMsg.State) == netlink.TCP_CLOSE {
70+
udpPort = fmt.Sprintf("%d", s.InetDiagMsg.ID.SourcePort)
71+
break
72+
}
73+
}
74+
if tcpPort == "" || udpPort == "" {
75+
return "", "", fmt.Errorf("could not find docker embedded resolver ports for %s (tcp=%q udp=%q)", loIP, tcpPort, udpPort)
76+
}
77+
return tcpPort, udpPort, nil
78+
}
79+
80+
// ClearRules removes DNAT rules dockerRules could have added.
81+
func clearRules() error {
82+
83+
// This will be run from kill() so no memory state could be used.
84+
// we list all IPtable rules from scratch.
85+
86+
// iptables-save output example:
87+
// -A PREROUTING -d 192.168.100.100/32 -i lo -p udp -j DNAT --to-destination 127.0.0.11:60269
88+
ipt, err := exec.LookPath("iptables-save")
89+
if err != nil {
90+
return err
91+
}
92+
out, err := exec.Command(ipt, "-t", "nat").Output() //nolint:gosec
93+
if err != nil {
94+
return fmt.Errorf("iptables-save failed: %w", err)
95+
}
96+
97+
var retErr error
98+
for line := range strings.SplitSeq(string(out), "\n") {
99+
fields := strings.Fields(line)
100+
if len(fields) < 2 || fields[0] != "-A" || fields[1] != "PREROUTING" {
101+
continue
102+
}
103+
if !strings.Contains(line, "-i lo") || !strings.Contains(line, "-j DNAT") {
104+
continue
105+
}
106+
args := append([]string{"-t", "nat", "-D"}, fields[1:]...)
107+
if err := ipTablesExec(args); err != nil {
108+
retErr = errors.Join(retErr, err)
109+
}
110+
}
111+
return retErr
112+
}

0 commit comments

Comments
 (0)