Skip to content

Commit dcebda4

Browse files
committed
feat: add interface report to coder netcheck
1 parent d0fc81a commit dcebda4

File tree

7 files changed

+300
-5
lines changed

7 files changed

+300
-5
lines changed

cli/netcheck.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
1212
"github.com/coder/coder/v2/codersdk"
13+
"github.com/coder/coder/v2/codersdk/healthsdk"
1314
"github.com/coder/coder/v2/codersdk/workspacesdk"
1415
"github.com/coder/serpent"
1516
)
@@ -34,11 +35,21 @@ func (r *RootCmd) netcheck() *serpent.Command {
3435

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

37-
var report derphealth.Report
38-
report.Run(ctx, &derphealth.ReportOptions{
38+
var derpReport derphealth.Report
39+
derpReport.Run(ctx, &derphealth.ReportOptions{
3940
DERPMap: connInfo.DERPMap,
4041
})
4142

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

cli/netcheck_test.go

Lines changed: 3 additions & 3 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,9 @@ type WorkspaceProxyReport struct {
269269
BaseReport
270270
WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"`
271271
}
272+
273+
// @typescript-ignore ClientNetcheckReport
274+
type ClientNetcheckReport struct {
275+
DERP DERPHealthReport `json:"derp"`
276+
Interfaces InterfacesReport `json:"interfaces"`
277+
}

codersdk/healthsdk/interfaces.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
// @typescript-ignore InterfacesReport
12+
type InterfacesReport struct {
13+
BaseReport
14+
Interfaces []Interface `json:"interfaces"`
15+
}
16+
17+
// @typescript-ignore Interface
18+
type Interface struct {
19+
Name string `json:"name"`
20+
MTU int `json:"mtu"`
21+
Addresses []string `json:"addresses"`
22+
}
23+
24+
func RunInterfacesReport() (InterfacesReport, error) {
25+
st, err := interfaces.GetState()
26+
if err != nil {
27+
return InterfacesReport{}, err
28+
}
29+
return generateInterfacesReport(st), nil
30+
}
31+
32+
func generateInterfacesReport(st *interfaces.State) (report InterfacesReport) {
33+
report.Severity = health.SeverityOK
34+
for name, iface := range st.Interface {
35+
// macOS has a ton of random interfaces, so to keep things helpful, let's filter out any
36+
// that:
37+
//
38+
// - are not enabled
39+
// - don't have any addresses
40+
// - have only link-local addresses (e.g. fe80:...)
41+
if (iface.Flags & net.FlagUp) == 0 {
42+
continue
43+
}
44+
addrs := st.InterfaceIPs[name]
45+
if len(addrs) == 0 {
46+
continue
47+
}
48+
var r bool
49+
healthIface := Interface{
50+
Name: iface.Name,
51+
MTU: iface.MTU,
52+
}
53+
for _, addr := range addrs {
54+
healthIface.Addresses = append(healthIface.Addresses, addr.String())
55+
if addr.Addr().IsLinkLocalUnicast() || addr.Addr().IsLinkLocalMulticast() {
56+
continue
57+
}
58+
r = true
59+
}
60+
if !r {
61+
continue
62+
}
63+
report.Interfaces = append(report.Interfaces, healthIface)
64+
if iface.MTU < 1378 {
65+
report.Severity = health.SeverityWarning
66+
report.Warnings = append(report.Warnings,
67+
health.Messagef(health.CodeInterfaceSmallMTU,
68+
"network interface %s has MTU %d (less than 1378), which may cause problems with direct connections", iface.Name, iface.MTU),
69+
)
70+
}
71+
}
72+
return report
73+
}
Lines changed: 192 additions & 0 deletions
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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,17 @@ 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
336+
the minimum MTU for Coder to establish direct connections without fragmentation.
337+
338+
**Solution:** Since IP fragmentation can be a source of performance problems, we
339+
recommend you disable the interface when using Coder or
340+
[disable direct connections](../../cli#--disable-direct-connections)
341+
331342
## EUNKNOWN
332343

333344
_Unknown Error_

0 commit comments

Comments
 (0)