Skip to content

feat!: add interface report to coder netcheck #13562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions cli/netcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/coder/coder/v2/coderd/healthcheck/derphealth"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/healthsdk"
"github.com/coder/coder/v2/codersdk/workspacesdk"
"github.com/coder/serpent"
)
Expand All @@ -34,11 +35,21 @@ func (r *RootCmd) netcheck() *serpent.Command {

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

var report derphealth.Report
report.Run(ctx, &derphealth.ReportOptions{
var derpReport derphealth.Report
derpReport.Run(ctx, &derphealth.ReportOptions{
DERPMap: connInfo.DERPMap,
})

ifReport, err := healthsdk.RunInterfacesReport()
if err != nil {
return xerrors.Errorf("failed to run interfaces report: %w", err)
}

report := healthsdk.ClientNetcheckReport{
DERP: healthsdk.DERPHealthReport(derpReport),
Interfaces: ifReport,
}

raw, err := json.MarshalIndent(report, "", " ")
if err != nil {
return err
Expand Down
6 changes: 3 additions & 3 deletions cli/netcheck_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ func TestNetcheck(t *testing.T) {

b := out.Bytes()
t.Log(string(b))
var report healthsdk.DERPHealthReport
var report healthsdk.ClientNetcheckReport
require.NoError(t, json.Unmarshal(b, &report))

// We do not assert that the report is healthy, just that
// it has the expected number of reports per region.
require.Len(t, report.Regions, 1+1) // 1 built-in region + 1 test-managed STUN region
for _, v := range report.Regions {
require.Len(t, report.DERP.Regions, 1+1) // 1 built-in region + 1 test-managed STUN region
for _, v := range report.DERP.Regions {
require.Len(t, v.NodeReports, len(v.Region.Nodes))
}
}
2 changes: 2 additions & 0 deletions coderd/healthcheck/health/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const (
CodeProvisionerDaemonsNoProvisionerDaemons Code = `EPD01`
CodeProvisionerDaemonVersionMismatch Code = `EPD02`
CodeProvisionerDaemonAPIMajorVersionDeprecated Code = `EPD03`

CodeInterfaceSmallMTU = `EIF01`
)

// Default docs URL
Expand Down
6 changes: 6 additions & 0 deletions codersdk/healthsdk/healthsdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,3 +269,9 @@ type WorkspaceProxyReport struct {
BaseReport
WorkspaceProxies codersdk.RegionsResponse[codersdk.WorkspaceProxy] `json:"workspace_proxies"`
}

// @typescript-ignore ClientNetcheckReport
type ClientNetcheckReport struct {
DERP DERPHealthReport `json:"derp"`
Interfaces InterfacesReport `json:"interfaces"`
}
73 changes: 73 additions & 0 deletions codersdk/healthsdk/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package healthsdk

import (
"net"

"tailscale.com/net/interfaces"

"github.com/coder/coder/v2/coderd/healthcheck/health"
)

// @typescript-ignore InterfacesReport
type InterfacesReport struct {
BaseReport
Interfaces []Interface `json:"interfaces"`
}

// @typescript-ignore Interface
type Interface struct {
Name string `json:"name"`
MTU int `json:"mtu"`
Addresses []string `json:"addresses"`
}

func RunInterfacesReport() (InterfacesReport, error) {
st, err := interfaces.GetState()
if err != nil {
return InterfacesReport{}, err
}
return generateInterfacesReport(st), nil
}

func generateInterfacesReport(st *interfaces.State) (report InterfacesReport) {
report.Severity = health.SeverityOK
for name, iface := range st.Interface {
// macOS has a ton of random interfaces, so to keep things helpful, let's filter out any
// that:
//
// - are not enabled
// - don't have any addresses
// - have only link-local addresses (e.g. fe80:...)
if (iface.Flags & net.FlagUp) == 0 {
continue
}
addrs := st.InterfaceIPs[name]
if len(addrs) == 0 {
continue
}
var r bool
healthIface := Interface{
Name: iface.Name,
MTU: iface.MTU,
}
for _, addr := range addrs {
healthIface.Addresses = append(healthIface.Addresses, addr.String())
if addr.Addr().IsLinkLocalUnicast() || addr.Addr().IsLinkLocalMulticast() {
continue
}
r = true
}
if !r {
continue
}
report.Interfaces = append(report.Interfaces, healthIface)
if iface.MTU < 1378 {
report.Severity = health.SeverityWarning
report.Warnings = append(report.Warnings,
health.Messagef(health.CodeInterfaceSmallMTU,
"network interface %s has MTU %d (less than 1378), which may cause problems with direct connections", iface.Name, iface.MTU),
)
}
}
return report
}
192 changes: 192 additions & 0 deletions codersdk/healthsdk/interfaces_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package healthsdk

import (
"net"
"net/netip"
"strings"
"testing"

"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
"tailscale.com/net/interfaces"

"github.com/coder/coder/v2/coderd/healthcheck/health"
)

func Test_generateInterfacesReport(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
state interfaces.State
severity health.Severity
expectedInterfaces []string
expectedWarnings []string
}{
{
name: "Empty",
state: interfaces.State{},
severity: health.SeverityOK,
expectedInterfaces: []string{},
},
{
name: "Normal",
state: interfaces.State{
Interface: map[string]interfaces.Interface{
"en0": {Interface: &net.Interface{
MTU: 1500,
Name: "en0",
Flags: net.FlagUp,
}},
"lo0": {Interface: &net.Interface{
MTU: 65535,
Name: "lo0",
Flags: net.FlagUp,
}},
},
InterfaceIPs: map[string][]netip.Prefix{
"en0": {
netip.MustParsePrefix("192.168.100.1/24"),
netip.MustParsePrefix("fe80::c13:1a92:3fa5:dd7e/64"),
},
"lo0": {
netip.MustParsePrefix("127.0.0.1/8"),
netip.MustParsePrefix("::1/128"),
netip.MustParsePrefix("fe80::1/64"),
},
},
},
severity: health.SeverityOK,
expectedInterfaces: []string{"en0", "lo0"},
},
{
name: "IgnoreDisabled",
state: interfaces.State{
Interface: map[string]interfaces.Interface{
"en0": {Interface: &net.Interface{
MTU: 1300,
Name: "en0",
Flags: 0,
}},
"lo0": {Interface: &net.Interface{
MTU: 65535,
Name: "lo0",
Flags: net.FlagUp,
}},
},
InterfaceIPs: map[string][]netip.Prefix{
"en0": {netip.MustParsePrefix("192.168.100.1/24")},
"lo0": {netip.MustParsePrefix("127.0.0.1/8")},
},
},
severity: health.SeverityOK,
expectedInterfaces: []string{"lo0"},
},
{
name: "IgnoreLinkLocalOnly",
state: interfaces.State{
Interface: map[string]interfaces.Interface{
"en0": {Interface: &net.Interface{
MTU: 1300,
Name: "en0",
Flags: net.FlagUp,
}},
"lo0": {Interface: &net.Interface{
MTU: 65535,
Name: "lo0",
Flags: net.FlagUp,
}},
},
InterfaceIPs: map[string][]netip.Prefix{
"en0": {netip.MustParsePrefix("fe80::1:1/64")},
"lo0": {netip.MustParsePrefix("127.0.0.1/8")},
},
},
severity: health.SeverityOK,
expectedInterfaces: []string{"lo0"},
},
{
name: "IgnoreNoAddress",
state: interfaces.State{
Interface: map[string]interfaces.Interface{
"en0": {Interface: &net.Interface{
MTU: 1300,
Name: "en0",
Flags: net.FlagUp,
}},
"lo0": {Interface: &net.Interface{
MTU: 65535,
Name: "lo0",
Flags: net.FlagUp,
}},
},
InterfaceIPs: map[string][]netip.Prefix{
"en0": {},
"lo0": {netip.MustParsePrefix("127.0.0.1/8")},
},
},
severity: health.SeverityOK,
expectedInterfaces: []string{"lo0"},
},
{
name: "SmallMTUTunnel",
state: interfaces.State{
Interface: map[string]interfaces.Interface{
"en0": {Interface: &net.Interface{
MTU: 1500,
Name: "en0",
Flags: net.FlagUp,
}},
"lo0": {Interface: &net.Interface{
MTU: 65535,
Name: "lo0",
Flags: net.FlagUp,
}},
"tun0": {Interface: &net.Interface{
MTU: 1280,
Name: "tun0",
Flags: net.FlagUp,
}},
},
InterfaceIPs: map[string][]netip.Prefix{
"en0": {netip.MustParsePrefix("192.168.100.1/24")},
"tun0": {netip.MustParsePrefix("10.3.55.9/8")},
"lo0": {netip.MustParsePrefix("127.0.0.1/8")},
},
},
severity: health.SeverityWarning,
expectedInterfaces: []string{"en0", "lo0", "tun0"},
expectedWarnings: []string{"tun0"},
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
r := generateInterfacesReport(&tc.state)
require.Equal(t, tc.severity, r.Severity)
gotInterfaces := []string{}
for _, i := range r.Interfaces {
gotInterfaces = append(gotInterfaces, i.Name)
}
slices.Sort(gotInterfaces)
slices.Sort(tc.expectedInterfaces)
require.Equal(t, tc.expectedInterfaces, gotInterfaces)

require.Len(t, r.Warnings, len(tc.expectedWarnings),
"expected %d warnings, got %d", len(tc.expectedWarnings), len(r.Warnings))
for _, name := range tc.expectedWarnings {
found := false
for _, w := range r.Warnings {
if strings.Contains(w.String(), name) {
found = true
break
}
}
if !found {
t.Errorf("missing warning for %s", name)
}
}
})
}
}
11 changes: 11 additions & 0 deletions docs/admin/healthcheck.md
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,17 @@ version of Coder.
> Note: This may be a transient issue if you are currently in the process of
> updating your deployment.

### EIF01

_Interface with Small MTU_

**Problem:** One or more local interfaces have MTU smaller than 1378, which is
the minimum MTU for Coder to establish direct connections without fragmentation.

**Solution:** Since IP fragmentation can be a source of performance problems, we
recommend you disable the interface when using Coder or
[disable direct connections](../../cli#--disable-direct-connections)

## EUNKNOWN

_Unknown Error_
Expand Down
Loading