Skip to content

Commit 65ed1ab

Browse files
authored
feat(qemu): add QEMU_NET_CIDR to override SLIRP internal network (#2564)
## What Add an opt-in QEMU_NET_CIDR environment variable that overrides SLIRP's default internal network (10.0.2.0/24) by appending net=<cidr> to the SLIRP netdev args. Validates the input as a sane IPv4 CIDR with prefix length between 8 and 30. ## Why SLIRP user-mode networking treats its default 10.0.2.0/24 network as part of its own NAT space. When the host needs to reach VPC-internal addresses that fall within the 10.0.0.0/8 range (e.g., a Private Service Connect endpoint at 10.10.0.108), SLIRP may not forward those connections to the host's network stack, causing the guest VM to time out. Setting QEMU_NET_CIDR to a non-conflicting range (e.g., 192.168.76.0/24) moves SLIRP's internal NAT off the 10.x.x.x space so 10.x.x.x addresses route through the host normally. ## Notes - Opt-in via environment variable; default behavior unchanged. - Follows the existing QEMU_DNS_SEARCH pattern for env-driven netdev configuration. - Input validation rejects IPv6, prefixes outside [8, 30], and any injection attempts via embedded commas or whitespace. Signed-off-by: jmeridth <jmeridth@gmail.com>
1 parent 1b5764a commit 65ed1ab

2 files changed

Lines changed: 130 additions & 0 deletions

File tree

pkg/container/qemu_runner.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,22 @@ func createMicroVM(ctx context.Context, cfg *Config) error {
798798
baseargs = append(baseargs, serialArgs...)
799799
// use -netdev + -device instead of -nic, as this is better supported by microvm machine type
800800
netdevArgs := "user,id=id1,hostfwd=tcp:" + cfg.SSHAddress + "-:22,hostfwd=tcp:" + cfg.SSHControlAddress + "-:2223"
801+
// QEMU_NET_CIDR overrides SLIRP's default internal network (10.0.2.0/24).
802+
// This is necessary when the host needs to reach VPC-internal addresses
803+
// that fall within the 10.0.0.0/8 range, since SLIRP treats its default
804+
// network as part of its own NAT space and may not correctly forward
805+
// connections to other 10.x.x.x addresses on the host's network.
806+
// The value must be a valid IPv4 CIDR. SLIRP automatically assigns the
807+
// gateway, DNS, and DHCP range based on the supplied network.
808+
// Example: QEMU_NET_CIDR="192.168.76.0/24"
809+
if netCIDR, ok := os.LookupEnv("QEMU_NET_CIDR"); ok {
810+
cidr, err := parseAndValidateNetCIDR(netCIDR)
811+
if err != nil {
812+
return fmt.Errorf("invalid QEMU_NET_CIDR value %q: %w", netCIDR, err)
813+
}
814+
log.Infof("qemu: QEMU_NET_CIDR set to %s, overriding SLIRP default network", cidr)
815+
netdevArgs += ",net=" + cidr
816+
}
801817
// QEMU_DNS_SEARCH allows configuring DNS search domains inside the guest VM.
802818
// This is useful for builds that need to resolve short hostnames via search
803819
// domains, or when the build environment requires specific DNS resolution
@@ -2543,6 +2559,36 @@ func parseDNSSearchDomains(input string) ([]string, error) {
25432559
return domains, nil
25442560
}
25452561

2562+
// parseAndValidateNetCIDR validates an IPv4 CIDR string for use as the SLIRP
2563+
// internal network. The CIDR must be a valid IPv4 network with a prefix length
2564+
// between 8 and 30 (SLIRP requires at least 4 usable addresses). The input is
2565+
// returned unchanged on success so it can be passed directly to SLIRP.
2566+
func parseAndValidateNetCIDR(input string) (string, error) {
2567+
input = strings.TrimSpace(input)
2568+
if input == "" {
2569+
return "", fmt.Errorf("empty CIDR")
2570+
}
2571+
2572+
ip, ipnet, err := net.ParseCIDR(input)
2573+
if err != nil {
2574+
return "", fmt.Errorf("parse CIDR: %w", err)
2575+
}
2576+
2577+
if ip.To4() == nil {
2578+
return "", fmt.Errorf("CIDR must be IPv4")
2579+
}
2580+
2581+
ones, bits := ipnet.Mask.Size()
2582+
if bits != 32 {
2583+
return "", fmt.Errorf("CIDR must be IPv4 (got %d-bit mask)", bits)
2584+
}
2585+
if ones < 8 || ones > 30 {
2586+
return "", fmt.Errorf("CIDR prefix length must be between 8 and 30 (got /%d)", ones)
2587+
}
2588+
2589+
return ipnet.String(), nil
2590+
}
2591+
25462592
// buildDNSSearchNetdevArgs constructs the QEMU netdev dnssearch options string.
25472593
// Returns empty string if no domains provided.
25482594
// Each domain produces a separate ",dnssearch=<domain>" option.

pkg/container/qemu_runner_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,90 @@ func TestBuildDNSSearchNetdevArgs(t *testing.T) {
492492
}
493493
}
494494

495+
func TestParseAndValidateNetCIDR(t *testing.T) {
496+
tests := []struct {
497+
name string
498+
input string
499+
expected string
500+
wantErr bool
501+
}{
502+
{
503+
name: "valid /24",
504+
input: "192.168.76.0/24",
505+
expected: "192.168.76.0/24",
506+
},
507+
{
508+
name: "valid /16",
509+
input: "192.168.0.0/16",
510+
expected: "192.168.0.0/16",
511+
},
512+
{
513+
name: "non-zero host bits normalized",
514+
input: "192.168.76.5/24",
515+
expected: "192.168.76.0/24",
516+
},
517+
{
518+
name: "whitespace trimmed",
519+
input: " 192.168.76.0/24 ",
520+
expected: "192.168.76.0/24",
521+
},
522+
{
523+
name: "empty",
524+
input: "",
525+
wantErr: true,
526+
},
527+
{
528+
name: "not a CIDR",
529+
input: "192.168.76.0",
530+
wantErr: true,
531+
},
532+
{
533+
name: "garbage",
534+
input: "not-a-cidr",
535+
wantErr: true,
536+
},
537+
{
538+
name: "IPv6",
539+
input: "fd00::/64",
540+
wantErr: true,
541+
},
542+
{
543+
name: "prefix too short",
544+
input: "10.0.0.0/7",
545+
wantErr: true,
546+
},
547+
{
548+
name: "prefix too long",
549+
input: "192.168.76.0/31",
550+
wantErr: true,
551+
},
552+
{
553+
name: "injection via comma",
554+
input: "192.168.76.0/24,dnssearch=evil.com",
555+
wantErr: true,
556+
},
557+
}
558+
559+
for _, tt := range tests {
560+
t.Run(tt.name, func(t *testing.T) {
561+
result, err := parseAndValidateNetCIDR(tt.input)
562+
if tt.wantErr {
563+
if err == nil {
564+
t.Errorf("parseAndValidateNetCIDR(%q) expected error, got nil with result %q", tt.input, result)
565+
}
566+
return
567+
}
568+
if err != nil {
569+
t.Errorf("parseAndValidateNetCIDR(%q) unexpected error: %v", tt.input, err)
570+
return
571+
}
572+
if result != tt.expected {
573+
t.Errorf("parseAndValidateNetCIDR(%q) = %q, expected %q", tt.input, result, tt.expected)
574+
}
575+
})
576+
}
577+
}
578+
495579
func TestGetPackageCacheSuffix(t *testing.T) {
496580
tests := []struct {
497581
name string

0 commit comments

Comments
 (0)