Skip to content

Commit d6b81b9

Browse files
authored
fix(qemu): pass SLIRP DNS address via kernel cmdline when QEMU_NET_CIDR is set (#2570)
* fix(qemu): pass SLIRP DNS address via kernel cmdline when QEMU_NET_CIDR is set ## What When QEMU_NET_CIDR overrides the SLIRP internal network, derive the correct DNS server address (network base + 3, per SLIRP convention) and pass it to the guest via the `dns=` kernel command line parameter. ## Why Guest init scripts typically write `/etc/resolv.conf` using the `dns=` kernel param with a fallback to `10.0.2.3` (SLIRP's default). When QEMU_NET_CIDR moves the SLIRP network to a different subnet (e.g., `192.168.76.0/24`), the default DNS address `10.0.2.3` no longer exists on that network and all DNS resolution inside the QEMU VM fails. This breaks git clones, submodule fetches, and any other network operation that requires name resolution. ## Notes - When QEMU_NET_CIDR is not set or empty, no `dns=` is added to the kernel cmdline. Zero behavior change for existing builds. - The `dns=` parameter follows QEMU/Linux kernel cmdline conventions and is already consumed by common guest init scripts. Signed-off-by: jmeridth <jmeridth@gmail.com> * fix(qemu): address review feedback on SLIRP DNS handling - Set dns= explicitly on QEMU netdev args so melange is authoritative about the DNS address rather than relying on SLIRP's internal default derivation, which uses a different algorithm for prefixes wider than /23 - Remove redundant ip/ipv4 nil-check in slirpDNSAddr, use binary.BigEndian for cleaner uint32 pack/unpack - Remove dead ipnet.Contains check (base+3 is always in-network for the /8-/28 range enforced by parseAndValidateNetCIDR) - Tighten parseAndValidateNetCIDR minimum prefix from /30 to /28 since SLIRP needs room for gateway (.2), DNS (.3), and DHCP range (.15+) - Add /22, /23, and /28 boundary test cases for slirpDNSAddr Signed-off-by: jmeridth <jmeridth@gmail.com> --------- Signed-off-by: jmeridth <jmeridth@gmail.com>
1 parent ad5661f commit d6b81b9

2 files changed

Lines changed: 137 additions & 5 deletions

File tree

pkg/container/qemu_runner.go

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"crypto/sha256"
2626
_ "embed"
2727
"encoding/base64"
28+
"encoding/binary"
2829
"encoding/pem"
2930
"errors"
3031
"fmt"
@@ -806,13 +807,19 @@ func createMicroVM(ctx context.Context, cfg *Config) error {
806807
// The value must be a valid IPv4 CIDR. SLIRP automatically assigns the
807808
// gateway, DNS, and DHCP range based on the supplied network.
808809
// Example: QEMU_NET_CIDR="192.168.76.0/24"
810+
var slirpDNS string
809811
if netCIDR, ok := os.LookupEnv("QEMU_NET_CIDR"); ok && netCIDR != "" {
810812
cidr, err := parseAndValidateNetCIDR(netCIDR)
811813
if err != nil {
812814
return fmt.Errorf("invalid QEMU_NET_CIDR value %q: %w", netCIDR, err)
813815
}
814-
log.Infof("qemu: QEMU_NET_CIDR set to %s, overriding SLIRP default network", cidr)
815-
netdevArgs += ",net=" + cidr
816+
dnsIP, err := slirpDNSAddr(cidr)
817+
if err != nil {
818+
return fmt.Errorf("deriving SLIRP DNS address from QEMU_NET_CIDR %q: %w", cidr, err)
819+
}
820+
slirpDNS = dnsIP
821+
log.Infof("qemu: QEMU_NET_CIDR set to %s, overriding SLIRP default network (dns=%s)", cidr, slirpDNS)
822+
netdevArgs += ",net=" + cidr + ",dns=" + slirpDNS
816823
}
817824
// QEMU_DNS_SEARCH allows configuring DNS search domains inside the guest VM.
818825
// This is useful for builds that need to resolve short hostnames via search
@@ -852,6 +859,13 @@ func createMicroVM(ctx context.Context, cfg *Config) error {
852859
cmdlineVar + " sshkey=" + sshkey +
853860
" melange_qemu_runner=1"
854861

862+
// When QEMU_NET_CIDR overrides the SLIRP network, the default DNS
863+
// address (10.0.2.3) no longer exists. Pass the correct address via
864+
// kernel cmdline so microvm-init writes it to /etc/resolv.conf.
865+
if slirpDNS != "" {
866+
kernelArgs += " dns=" + slirpDNS
867+
}
868+
855869
// Check for TESTING environment variable and pass it to microvm-init
856870
// TESTING must be a number (0 for disabled, non-zero for enabled)
857871
if testingValue, ok := os.LookupEnv("TESTING"); ok {
@@ -2582,13 +2596,37 @@ func parseAndValidateNetCIDR(input string) (string, error) {
25822596
if bits != 32 {
25832597
return "", fmt.Errorf("CIDR must be IPv4 (got %d-bit mask)", bits)
25842598
}
2585-
if ones < 8 || ones > 30 {
2586-
return "", fmt.Errorf("CIDR prefix length must be between 8 and 30 (got /%d)", ones)
2599+
if ones < 8 || ones > 28 {
2600+
return "", fmt.Errorf("CIDR prefix length must be between 8 and 28 (got /%d)", ones)
25872601
}
25882602

25892603
return ipnet.String(), nil
25902604
}
25912605

2606+
// slirpDNSAddr returns the DNS server address to use for the SLIRP network.
2607+
// The address is network base + 3, placed at offset .3 within the subnet.
2608+
// The caller must also pass this address via the QEMU netdev dns= option so
2609+
// SLIRP serves it, and via the kernel cmdline dns= parameter so the guest
2610+
// init writes it to /etc/resolv.conf. For example, 192.168.76.0/24 yields
2611+
// 192.168.76.3. The input must be a valid IPv4 CIDR (as returned by
2612+
// parseAndValidateNetCIDR).
2613+
func slirpDNSAddr(cidr string) (string, error) {
2614+
_, ipnet, err := net.ParseCIDR(cidr)
2615+
if err != nil {
2616+
return "", fmt.Errorf("parse CIDR: %w", err)
2617+
}
2618+
base := ipnet.IP.To4()
2619+
if base == nil {
2620+
return "", fmt.Errorf("not IPv4")
2621+
}
2622+
2623+
n := binary.BigEndian.Uint32(base) + 3
2624+
dns := make(net.IP, 4)
2625+
binary.BigEndian.PutUint32(dns, n)
2626+
2627+
return dns.String(), nil
2628+
}
2629+
25922630
// buildDNSSearchNetdevArgs constructs the QEMU netdev dnssearch options string.
25932631
// Returns empty string if no domains provided.
25942632
// Each domain produces a separate ",dnssearch=<domain>" option.

pkg/container/qemu_runner_test.go

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,10 +545,25 @@ func TestParseAndValidateNetCIDR(t *testing.T) {
545545
wantErr: true,
546546
},
547547
{
548-
name: "prefix too long",
548+
name: "prefix too long /29",
549+
input: "192.168.76.0/29",
550+
wantErr: true,
551+
},
552+
{
553+
name: "prefix too long /30",
554+
input: "192.168.76.0/30",
555+
wantErr: true,
556+
},
557+
{
558+
name: "prefix too long /31",
549559
input: "192.168.76.0/31",
550560
wantErr: true,
551561
},
562+
{
563+
name: "valid /28 boundary",
564+
input: "192.168.76.0/28",
565+
expected: "192.168.76.0/28",
566+
},
552567
{
553568
name: "injection via comma",
554569
input: "192.168.76.0/24,dnssearch=evil.com",
@@ -576,6 +591,85 @@ func TestParseAndValidateNetCIDR(t *testing.T) {
576591
}
577592
}
578593

594+
func TestSlirpDNSAddr(t *testing.T) {
595+
tests := []struct {
596+
name string
597+
cidr string
598+
expected string
599+
wantErr bool
600+
}{
601+
{
602+
name: "standard /24",
603+
cidr: "192.168.76.0/24",
604+
expected: "192.168.76.3",
605+
},
606+
{
607+
name: "default SLIRP network",
608+
cidr: "10.0.2.0/24",
609+
expected: "10.0.2.3",
610+
},
611+
{
612+
name: "/16 network",
613+
cidr: "172.16.0.0/16",
614+
expected: "172.16.0.3",
615+
},
616+
{
617+
name: "/8 network",
618+
cidr: "10.0.0.0/8",
619+
expected: "10.0.0.3",
620+
},
621+
{
622+
name: "non-zero third octet",
623+
cidr: "192.168.1.0/24",
624+
expected: "192.168.1.3",
625+
},
626+
{
627+
name: "/22 boundary",
628+
cidr: "192.168.0.0/22",
629+
expected: "192.168.0.3",
630+
},
631+
{
632+
name: "/23 boundary",
633+
cidr: "192.168.0.0/23",
634+
expected: "192.168.0.3",
635+
},
636+
{
637+
name: "/28 smallest valid",
638+
cidr: "192.168.76.0/28",
639+
expected: "192.168.76.3",
640+
},
641+
{
642+
name: "invalid CIDR",
643+
cidr: "not-a-cidr",
644+
wantErr: true,
645+
},
646+
{
647+
name: "empty input",
648+
cidr: "",
649+
wantErr: true,
650+
},
651+
}
652+
653+
for _, tt := range tests {
654+
t.Run(tt.name, func(t *testing.T) {
655+
result, err := slirpDNSAddr(tt.cidr)
656+
if tt.wantErr {
657+
if err == nil {
658+
t.Errorf("slirpDNSAddr(%q) expected error, got %q", tt.cidr, result)
659+
}
660+
return
661+
}
662+
if err != nil {
663+
t.Errorf("slirpDNSAddr(%q) unexpected error: %v", tt.cidr, err)
664+
return
665+
}
666+
if result != tt.expected {
667+
t.Errorf("slirpDNSAddr(%q) = %q, expected %q", tt.cidr, result, tt.expected)
668+
}
669+
})
670+
}
671+
}
672+
579673
func TestGetPackageCacheSuffix(t *testing.T) {
580674
tests := []struct {
581675
name string

0 commit comments

Comments
 (0)