Skip to content

Commit c11bf03

Browse files
Use kernel-range form for DNS-materialized remoteports
vastnfs rejects comma-separated IP lists in remoteports= with EINVAL — kernel mount returns "Invalid argument" and the pod stays in ContainerCreating with FailedMount events. Only the dash-range form "<low>-<high>" is accepted; the kernel expands it to every IP in the inclusive range. Empirically observed on a dlim CMK in eu-iceland1-a (ICAT) with a DnsName-populated disk: net.LookupIP returned the 16 ICAT-2 VIPs (172.27.255.18-.33) in random order; the previous materialize path joined them with commas, producing a value the kernel rejected on every mount attempt. Fix: sort the IPv4 results by network-byte-order value and emit the range as "<first>-<last>". Host becomes the lowest IP. Single-IP responses degenerate to "remoteports=<ip>" which the kernel also accepts. Coverage: extend resolve_test.go with TestMaterializeNFSTarget_ SortsOutOfOrderResponse and rename DNSResolvesToList -> DNSResolvesToRange so the assertion matches what vastnfs actually requires. Refs: CRUSOE-70481
1 parent b1b4250 commit c11bf03

2 files changed

Lines changed: 59 additions & 9 deletions

File tree

internal/node/fs/resolve.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package fs
22

33
import (
4+
"bytes"
45
"errors"
56
"fmt"
67
"net"
7-
"strings"
8+
"sort"
89
)
910

1011
// ErrNoUsableNFSAddress is returned when DNS resolution yields no usable IPv4
@@ -30,12 +31,17 @@ var lookupIP = net.LookupIP
3031
// If remotePorts == "dns", the hostname is resolved via lookupIP, IPv4-mapped
3132
// IPv6 addresses are normalized to IPv4, unspecified addresses (::, 0.0.0.0)
3233
// and non-IPv4 results are filtered out, and the result is returned as:
33-
// - newHost: the first valid IPv4, suitable as the host portion of the
34+
// - newHost: the lowest-valued IPv4, suitable as the host portion of the
3435
// mount source string ("<ip>:/volumes/<id>"), so that busybox-mount in
3536
// the Alpine CSI pod never needs to do its own getaddrinfo (closes the
3637
// INC-450 musl REFUSED surface).
37-
// - newRemotePorts: comma-separated list of all valid IPv4s, suitable for
38-
// the kernel's remoteports= option.
38+
// - newRemotePorts: kernel-range form "<minIP>-<maxIP>" suitable for the
39+
// vastnfs remoteports= option, which the kernel expands to every IP in
40+
// the inclusive range. NOT a comma-separated list: vastnfs rejects
41+
// comma form with EINVAL (kernel mount returns "Invalid argument").
42+
// If the DNS response is not contiguous, the range may span IPs that
43+
// are not actual NFS endpoints; those expand harmlessly because the
44+
// NFS client only opens connections that the server accepts.
3945
//
4046
// Returns ErrNoUsableNFSAddress (wrapped) when the hostname has no usable
4147
// IPv4 records after filtering. Callers should log the error and fall back
@@ -67,5 +73,19 @@ func materializeNFSTarget(host, remotePorts string) (newHost, newRemotePorts str
6773
return host, remotePorts, fmt.Errorf("%w: %s", ErrNoUsableNFSAddress, host)
6874
}
6975

70-
return ipv4s[0], strings.Join(ipv4s, ","), nil
76+
// Sort IPv4 strings by network-byte-order value so the kernel-range form
77+
// always reflects (lowest, highest). net.ParseIP returns a 16-byte
78+
// IPv6-mapped form even for IPv4 inputs; ip.To4() returns the canonical
79+
// 4-byte form for byte-wise comparison.
80+
sort.Slice(ipv4s, func(i, j int) bool {
81+
return bytes.Compare(net.ParseIP(ipv4s[i]).To4(), net.ParseIP(ipv4s[j]).To4()) < 0
82+
})
83+
84+
first := ipv4s[0]
85+
last := ipv4s[len(ipv4s)-1]
86+
if first == last {
87+
return first, first, nil
88+
}
89+
90+
return first, fmt.Sprintf("%s-%s", first, last), nil
7191
}

internal/node/fs/resolve_test.go

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func TestMaterializeNFSTarget_IPListPassthrough(t *testing.T) {
6161
}
6262

6363
//nolint:paralleltest // mutates package-level lookupIP via fs.SetLookupIP
64-
func TestMaterializeNFSTarget_DNSResolvesToList(t *testing.T) {
64+
func TestMaterializeNFSTarget_DNSResolvesToRange(t *testing.T) {
6565
const lookupHost = "nfs.crusoecloudcompute.com"
6666
withStubLookupIP(t, func(host string) ([]net.IP, error) {
6767
if host != lookupHost {
@@ -76,10 +76,40 @@ func TestMaterializeNFSTarget_DNSResolvesToList(t *testing.T) {
7676
t.Fatalf("unexpected error: %v", err)
7777
}
7878
if gotHost != testIPv4A {
79-
t.Errorf("host = %q, want %q (first IPv4)", gotHost, testIPv4A)
79+
t.Errorf("host = %q, want %q (lowest IPv4 by network byte order)", gotHost, testIPv4A)
8080
}
81-
if gotPorts != testIPv4A+","+testIPv4B {
82-
t.Errorf("remotePorts = %q, want %q", gotPorts, testIPv4A+","+testIPv4B)
81+
// vastnfs rejects comma-separated lists with EINVAL; only the kernel-range
82+
// form "<low>-<high>" is accepted. See CRUSOE-70481 dlim ICAT test
83+
// 2026-05-29 for the empirical failure that motivated this assertion.
84+
want := testIPv4A + "-" + testIPv4B
85+
if gotPorts != want {
86+
t.Errorf("remotePorts = %q, want %q (dash-range form, NOT comma-separated)", gotPorts, want)
87+
}
88+
}
89+
90+
//nolint:paralleltest // mutates package-level lookupIP via fs.SetLookupIP
91+
// TestMaterializeNFSTarget_SortsOutOfOrderResponse guards against the kernel
92+
// receiving "<high>-<low>" — the range form requires the lower IP first, and
93+
// net.LookupIP is documented to return addresses in unspecified order. This
94+
// matches the dlim ICAT response we observed (16 VIPs returned in shuffled
95+
// order) which would otherwise produce a malformed range like
96+
// "172.27.255.33-172.27.255.18".
97+
func TestMaterializeNFSTarget_SortsOutOfOrderResponse(t *testing.T) {
98+
withStubLookupIP(t, func(_ string) ([]net.IP, error) {
99+
// Deliberately reverse order: DNS hands us testIPv4B (higher) first.
100+
return []net.IP{net.ParseIP(testIPv4B), net.ParseIP(testIPv4A)}, nil
101+
})
102+
103+
gotHost, gotPorts, err := fs.MaterializeNFSTarget(testHost, testDNS)
104+
if err != nil {
105+
t.Fatalf("unexpected error: %v", err)
106+
}
107+
if gotHost != testIPv4A {
108+
t.Errorf("host = %q, want %q (must be the lowest IP after sort)", gotHost, testIPv4A)
109+
}
110+
want := testIPv4A + "-" + testIPv4B
111+
if gotPorts != want {
112+
t.Errorf("remotePorts = %q, want %q (sorted dash-range)", gotPorts, want)
83113
}
84114
}
85115

0 commit comments

Comments
 (0)