Skip to content

Commit 94b740d

Browse files
committed
feat: add interface report to coder netcheck
1 parent a11f8b0 commit 94b740d

File tree

7 files changed

+297
-5
lines changed

7 files changed

+297
-5
lines changed

cli/netcheck.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"time"
88

9+
"github.com/coder/coder/v2/codersdk/healthsdk"
10+
911
"golang.org/x/xerrors"
1012

1113
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
@@ -34,11 +36,21 @@ func (r *RootCmd) netcheck() *serpent.Command {
3436

3537
_, _ = fmt.Fprint(inv.Stderr, "Gathering a network report. This may take a few seconds...\n\n")
3638

37-
var report derphealth.Report
38-
report.Run(ctx, &derphealth.ReportOptions{
39+
var derpReport derphealth.Report
40+
derpReport.Run(ctx, &derphealth.ReportOptions{
3941
DERPMap: connInfo.DERPMap,
4042
})
4143

44+
ifReport, err := healthsdk.RunInterfacesReport()
45+
if err != nil {
46+
return xerrors.Errorf("failed to run interfaces report: %w", err)
47+
}
48+
49+
report := healthsdk.ClientNetcheckReport{
50+
DERP: healthsdk.DERPHealthReport(derpReport),
51+
Interfaces: ifReport,
52+
}
53+
4254
raw, err := json.MarshalIndent(report, "", " ")
4355
if err != nil {
4456
return err

cli/netcheck_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ func TestNetcheck(t *testing.T) {
2626

2727
b := out.Bytes()
2828
t.Log(string(b))
29-
var report healthsdk.DERPHealthReport
29+
var report healthsdk.ClientNetcheckReport
3030
require.NoError(t, json.Unmarshal(b, &report))
3131

3232
// We do not assert that the report is healthy, just that
3333
// it has the expected number of reports per region.
34-
require.Len(t, report.Regions, 1+1) // 1 built-in region + 1 test-managed STUN region
35-
for _, v := range report.Regions {
34+
require.Len(t, report.DERP.Regions, 1+1) // 1 built-in region + 1 test-managed STUN region
35+
for _, v := range report.DERP.Regions {
3636
require.Len(t, v.NodeReports, len(v.Region.Nodes))
3737
}
3838
}

coderd/healthcheck/health/model.go

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ const (
4343
CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01`
4444
CodeProvisionerDaemonVersionMismatch Code = `EPD02`
4545
CodeProvisionerDaemonAPIMajorVersionDeprecated Code = `EPD03`
46+
47+
CodeInterfaceSmallMTU = `EIF01`
4648
)
4749

4850
// Default docs URL

codersdk/healthsdk/healthsdk.go

+5
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,8 @@ type WorkspaceProxyReport struct {
269269
BaseReport
270270
WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"`
271271
}
272+
273+
type ClientNetcheckReport struct {
274+
DERP DERPHealthReport `json:"derp"`
275+
Interfaces InterfacesReport `json:"interfaces"`
276+
}

codersdk/healthsdk/interfaces.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package healthsdk
2+
3+
import (
4+
"net"
5+
6+
"tailscale.com/net/interfaces"
7+
8+
"github.com/coder/coder/v2/coderd/healthcheck/health"
9+
)
10+
11+
type InterfacesReport struct {
12+
BaseReport
13+
Interfaces []Interface `json:"interfaces"`
14+
}
15+
16+
type Interface struct {
17+
Name string `json:"name"`
18+
MTU int `json:"mtu"`
19+
Addresses []string `json:"addresses"`
20+
}
21+
22+
func RunInterfacesReport() (InterfacesReport, error) {
23+
st, err := interfaces.GetState()
24+
if err != nil {
25+
return InterfacesReport{}, err
26+
}
27+
return generateInterfacesReport(st), nil
28+
}
29+
30+
func generateInterfacesReport(st *interfaces.State) (report InterfacesReport) {
31+
report.Severity = health.SeverityOK
32+
for name, iface := range st.Interface {
33+
// macOS has a ton of random interfaces, so to keep things helpful, let's filter out any
34+
// that:
35+
//
36+
// - are not enabled
37+
// - don't have any addresses
38+
// - have only link-local addresses (e.g. fe80:...)
39+
if (iface.Flags & net.FlagUp) == 0 {
40+
continue
41+
}
42+
addrs := st.InterfaceIPs[name]
43+
if len(addrs) == 0 {
44+
continue
45+
}
46+
var r bool
47+
healthIface := Interface{
48+
Name: iface.Name,
49+
MTU: iface.MTU,
50+
}
51+
for _, addr := range addrs {
52+
healthIface.Addresses = append(healthIface.Addresses, addr.String())
53+
if addr.Addr().IsLinkLocalUnicast() || addr.Addr().IsLinkLocalMulticast() {
54+
continue
55+
}
56+
r = true
57+
}
58+
if !r {
59+
continue
60+
}
61+
report.Interfaces = append(report.Interfaces, healthIface)
62+
if iface.MTU < 1378 {
63+
report.Severity = health.SeverityWarning
64+
report.Warnings = append(report.Warnings,
65+
health.Messagef(health.CodeInterfaceSmallMTU,
66+
"network interface %s has MTU %d (less than 1378), which may cause problems with direct connections", iface.Name, iface.MTU),
67+
)
68+
}
69+
}
70+
return report
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package healthsdk
2+
3+
import (
4+
"net"
5+
"net/netip"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
"golang.org/x/exp/slices"
11+
"tailscale.com/net/interfaces"
12+
13+
"github.com/coder/coder/v2/coderd/healthcheck/health"
14+
)
15+
16+
func Test_generateInterfacesReport(t *testing.T) {
17+
t.Parallel()
18+
testCases := []struct {
19+
name string
20+
state interfaces.State
21+
severity health.Severity
22+
expectedInterfaces []string
23+
expectedWarnings []string
24+
}{
25+
{
26+
name: "Empty",
27+
state: interfaces.State{},
28+
severity: health.SeverityOK,
29+
expectedInterfaces: []string{},
30+
},
31+
{
32+
name: "Normal",
33+
state: interfaces.State{
34+
Interface: map[string]interfaces.Interface{
35+
"en0": {Interface: &net.Interface{
36+
MTU: 1500,
37+
Name: "en0",
38+
Flags: net.FlagUp,
39+
}},
40+
"lo0": {Interface: &net.Interface{
41+
MTU: 65535,
42+
Name: "lo0",
43+
Flags: net.FlagUp,
44+
}},
45+
},
46+
InterfaceIPs: map[string][]netip.Prefix{
47+
"en0": {
48+
netip.MustParsePrefix("192.168.100.1/24"),
49+
netip.MustParsePrefix("fe80::c13:1a92:3fa5:dd7e/64"),
50+
},
51+
"lo0": {
52+
netip.MustParsePrefix("127.0.0.1/8"),
53+
netip.MustParsePrefix("::1/128"),
54+
netip.MustParsePrefix("fe80::1/64"),
55+
},
56+
},
57+
},
58+
severity: health.SeverityOK,
59+
expectedInterfaces: []string{"en0", "lo0"},
60+
},
61+
{
62+
name: "IgnoreDisabled",
63+
state: interfaces.State{
64+
Interface: map[string]interfaces.Interface{
65+
"en0": {Interface: &net.Interface{
66+
MTU: 1300,
67+
Name: "en0",
68+
Flags: 0,
69+
}},
70+
"lo0": {Interface: &net.Interface{
71+
MTU: 65535,
72+
Name: "lo0",
73+
Flags: net.FlagUp,
74+
}},
75+
},
76+
InterfaceIPs: map[string][]netip.Prefix{
77+
"en0": {netip.MustParsePrefix("192.168.100.1/24")},
78+
"lo0": {netip.MustParsePrefix("127.0.0.1/8")},
79+
},
80+
},
81+
severity: health.SeverityOK,
82+
expectedInterfaces: []string{"lo0"},
83+
},
84+
{
85+
name: "IgnoreLinkLocalOnly",
86+
state: interfaces.State{
87+
Interface: map[string]interfaces.Interface{
88+
"en0": {Interface: &net.Interface{
89+
MTU: 1300,
90+
Name: "en0",
91+
Flags: net.FlagUp,
92+
}},
93+
"lo0": {Interface: &net.Interface{
94+
MTU: 65535,
95+
Name: "lo0",
96+
Flags: net.FlagUp,
97+
}},
98+
},
99+
InterfaceIPs: map[string][]netip.Prefix{
100+
"en0": {netip.MustParsePrefix("fe80::1:1/64")},
101+
"lo0": {netip.MustParsePrefix("127.0.0.1/8")},
102+
},
103+
},
104+
severity: health.SeverityOK,
105+
expectedInterfaces: []string{"lo0"},
106+
},
107+
{
108+
name: "IgnoreNoAddress",
109+
state: interfaces.State{
110+
Interface: map[string]interfaces.Interface{
111+
"en0": {Interface: &net.Interface{
112+
MTU: 1300,
113+
Name: "en0",
114+
Flags: net.FlagUp,
115+
}},
116+
"lo0": {Interface: &net.Interface{
117+
MTU: 65535,
118+
Name: "lo0",
119+
Flags: net.FlagUp,
120+
}},
121+
},
122+
InterfaceIPs: map[string][]netip.Prefix{
123+
"en0": {},
124+
"lo0": {netip.MustParsePrefix("127.0.0.1/8")},
125+
},
126+
},
127+
severity: health.SeverityOK,
128+
expectedInterfaces: []string{"lo0"},
129+
},
130+
{
131+
name: "SmallMTUTunnel",
132+
state: interfaces.State{
133+
Interface: map[string]interfaces.Interface{
134+
"en0": {Interface: &net.Interface{
135+
MTU: 1500,
136+
Name: "en0",
137+
Flags: net.FlagUp,
138+
}},
139+
"lo0": {Interface: &net.Interface{
140+
MTU: 65535,
141+
Name: "lo0",
142+
Flags: net.FlagUp,
143+
}},
144+
"tun0": {Interface: &net.Interface{
145+
MTU: 1280,
146+
Name: "tun0",
147+
Flags: net.FlagUp,
148+
}},
149+
},
150+
InterfaceIPs: map[string][]netip.Prefix{
151+
"en0": {netip.MustParsePrefix("192.168.100.1/24")},
152+
"tun0": {netip.MustParsePrefix("10.3.55.9/8")},
153+
"lo0": {netip.MustParsePrefix("127.0.0.1/8")},
154+
},
155+
},
156+
severity: health.SeverityWarning,
157+
expectedInterfaces: []string{"en0", "lo0", "tun0"},
158+
expectedWarnings: []string{"tun0"},
159+
},
160+
}
161+
162+
for _, tc := range testCases {
163+
tc := tc
164+
t.Run(tc.name, func(t *testing.T) {
165+
t.Parallel()
166+
r := generateInterfacesReport(&tc.state)
167+
require.Equal(t, tc.severity, r.Severity)
168+
gotInterfaces := []string{}
169+
for _, i := range r.Interfaces {
170+
gotInterfaces = append(gotInterfaces, i.Name)
171+
}
172+
slices.Sort(gotInterfaces)
173+
slices.Sort(tc.expectedInterfaces)
174+
require.Equal(t, tc.expectedInterfaces, gotInterfaces)
175+
176+
require.Len(t, r.Warnings, len(tc.expectedWarnings),
177+
"expected %d warnings, got %d", len(tc.expectedWarnings), len(r.Warnings))
178+
for _, name := range tc.expectedWarnings {
179+
found := false
180+
for _, w := range r.Warnings {
181+
if strings.Contains(w.String(), name) {
182+
found = true
183+
break
184+
}
185+
}
186+
if !found {
187+
t.Errorf("missing warning for %s", name)
188+
}
189+
}
190+
})
191+
}
192+
}

docs/admin/healthcheck.md

+10
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,16 @@ version of Coder.
328328
> Note: This may be a transient issue if you are currently in the process of
329329
> updating your deployment.
330330
331+
### EIF01
332+
333+
_Interface with Small MTU_
334+
335+
**Problem:** One or more local interfaces have MTU smaller than 1378, which is the minimum MTU for
336+
Coder to establish direct connections without fragmentation.
337+
338+
**Solution:** Since IP fragmentation can be a source of performance problems, we recommend you
339+
disable the interface when using Coder or [disable direct connections](./cli#--disable-direct-connections)
340+
331341
## EUNKNOWN
332342

333343
_Unknown Error_

0 commit comments

Comments
 (0)