diff --git a/server/handlers/wireguard/analyzer/analyzer.go b/server/handlers/wireguard/analyzer/analyzer.go new file mode 100644 index 0000000..fad1cf4 --- /dev/null +++ b/server/handlers/wireguard/analyzer/analyzer.go @@ -0,0 +1,45 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/indexes" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +type analyzerContext struct { + document *wireguard.WGDocument + diagnostics []protocol.Diagnostic +} + +func Analyze( + d *wireguard.WGDocument, +) []protocol.Diagnostic { + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeStructureIsValid(ctx) + + if len(ctx.diagnostics) > 0 { + return ctx.diagnostics + } + + i, indexErrors := indexes.CreateIndexes(d.Config) + + if len(indexErrors) > 0 { + return common.ErrsToDiagnostics(indexErrors) + } + + d.Indexes = i + + analyzeInterfaceSection(ctx) + analyzeDNSPropertyContainsFallback(ctx) + analyzeKeepAlivePropertyIsSet(ctx) + analyzeSymmetricPropertiesSet(ctx) + analyzeDuplicateAllowedIPs(ctx) + + return ctx.diagnostics +} diff --git a/server/handlers/wireguard/analyzer/analyzer_test.go b/server/handlers/wireguard/analyzer/analyzer_test.go new file mode 100644 index 0000000..50ac57c --- /dev/null +++ b/server/handlers/wireguard/analyzer/analyzer_test.go @@ -0,0 +1,57 @@ +package analyzer + +import ( + "config-lsp/handlers/wireguard/parser" + "config-lsp/utils" + "testing" +) + +func TestMultipleIntefaces(t *testing.T) { + content := utils.Dedent(` +[Interface] +PrivateKey = abc + +[Interface] +PrivateKey = def +`) + p := parser.CreateWireguardParser() + p.ParseFromString(content) + + diagnostics := Analyze(p) + + if len(diagnostics) == 0 { + t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) + } +} + +func TestInvalidValue(t *testing.T) { + content := utils.Dedent(` +[Interface] +DNS = nope +`) + p := parser.CreateWireguardParser() + p.ParseFromString(content) + + diagnostics := Analyze(p) + + if len(diagnostics) == 0 { + t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) + } +} + +func TestDuplicateProperties(t *testing.T) { + content := utils.Dedent(` +[Interface] +PrivateKey = abc +DNS = 1.1.1.1 +PrivateKey = def +`) + p := parser.CreateWireguardParser() + p.ParseFromString(content) + + diagnostics := Analyze(p) + + if len(diagnostics) == 0 { + t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) + } +} diff --git a/server/handlers/wireguard/analyzer/property.go b/server/handlers/wireguard/analyzer/property.go new file mode 100644 index 0000000..2a80141 --- /dev/null +++ b/server/handlers/wireguard/analyzer/property.go @@ -0,0 +1,136 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/utils" + "context" + "fmt" + "net/netip" + "strings" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeDNSPropertyContainsFallback( + ctx *analyzerContext, +) { + sections, found := ctx.document.Indexes.SectionsByName["Interface"] + + if !found { + return + } + + interfaceSection := sections[0] + + property := interfaceSection.FindFirstPropertyByName("DNS") + + if property == nil { + return + } + + dnsAmount := len(strings.Split(property.Value.Value, ",")) + + if dnsAmount == 1 { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "There is only one DNS server specified. It is recommended to set up fallback DNS servers", + Severity: &common.SeverityHint, + Range: property.Value.ToLSPRange(), + }) + } +} + +func analyzeKeepAlivePropertyIsSet( + ctx *analyzerContext, +) { + for _, section := range ctx.document.Indexes.SectionsByName["Peer"] { + // If an endpoint is set, then we should only check for the keepalive property + if section.FindFirstPropertyByName("Endpoint") != nil && section.FindFirstPropertyByName("PersistentKeepalive") == nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PersistentKeepalive is not set. It is recommended to set this property, as it helps to maintain the connection when users are behind NAT", + Severity: &common.SeverityHint, + Range: section.Header.ToLSPRange(), + }) + } + } +} + +func analyzeSymmetricPropertiesSet( + ctx *analyzerContext, +) { + for _, section := range ctx.document.Indexes.SectionsByName["Interface"] { + preUpProperty := section.FindFirstPropertyByName("PreUp") + preDownProperty := section.FindFirstPropertyByName("PreDown") + + postUpProperty := section.FindFirstPropertyByName("PostUp") + postDownProperty := section.FindFirstPropertyByName("PostDown") + + if preUpProperty != nil && preDownProperty == nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PreUp is set, but PreDown is not. It is recommended to set both properties symmetrically", + Range: preUpProperty.ToLSPRange(), + Severity: &common.SeverityHint, + }) + } else if preUpProperty == nil && preDownProperty != nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PreDown is set, but PreUp is not. It is recommended to set both properties symmetrically", + Range: preDownProperty.ToLSPRange(), + Severity: &common.SeverityHint, + }) + } + + if postUpProperty != nil && postDownProperty == nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PostUp is set, but PostDown is not. It is recommended to set both properties symmetrically", + Range: postUpProperty.ToLSPRange(), + Severity: &common.SeverityHint, + }) + } else if postUpProperty == nil && postDownProperty != nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PostDown is set, but PostUp is not. It is recommended to set both properties symmetrically", + Range: postDownProperty.ToLSPRange(), + Severity: &common.SeverityHint, + }) + } + } +} + +// Strategy +// Simply compare the host bits of the IP addresses. +// Use a binary tree to store the host bits. +func analyzeDuplicateAllowedIPs( + ctx *analyzerContext, +) { + ipHostSet := utils.CreateIPv4HostSet() + + for _, section := range ctx.document.Indexes.SectionsByName["Peer"] { + property := section.FindFirstPropertyByName("AllowedIPs") + + if property == nil { + continue + } + + ipAddress, err := netip.ParsePrefix(property.Value.Value) + + if err != nil { + // This should not happen... + continue + } + + if ipContext, _ := ipHostSet.ContainsIP(ipAddress); ipContext != nil { + definedLine := (*ipContext).Value("line").(uint32) + + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: fmt.Sprintf("This IP range is already covered on line %d", definedLine+1), + Severity: &common.SeverityError, + Range: property.ToLSPRange(), + }) + } else { + ipContext := context.WithValue(context.Background(), "line", property.Start.Line) + + ipHostSet.AddIP( + ipAddress, + ipContext, + ) + } + } +} diff --git a/server/handlers/wireguard/analyzer/section.go b/server/handlers/wireguard/analyzer/section.go new file mode 100644 index 0000000..2587183 --- /dev/null +++ b/server/handlers/wireguard/analyzer/section.go @@ -0,0 +1,18 @@ +package analyzer + +import ( + "config-lsp/common" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeInterfaceSection(ctx *analyzerContext) { + sections := ctx.document.Indexes.SectionsByName["Interface"] + if len(sections) > 1 { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "Only one [Interface] section is allowed", + Severity: &common.SeverityError, + Range: sections[1].Header.ToLSPRange(), + }) + } +} diff --git a/server/handlers/wireguard/analyzer/structure.go b/server/handlers/wireguard/analyzer/structure.go new file mode 100644 index 0000000..58a9c9e --- /dev/null +++ b/server/handlers/wireguard/analyzer/structure.go @@ -0,0 +1,83 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/handlers/wireguard/ast" + "config-lsp/handlers/wireguard/fields" + "config-lsp/utils" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeStructureIsValid(ctx *analyzerContext) { + for _, section := range ctx.document.Config.Sections { + // Whether to check if the property is allowed in the section + checkAllowedProperty := true + + if section.Header.Name == "" { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "This section is missing a name", + Range: section.Header.ToLSPRange(), + Severity: &common.SeverityError, + }) + } else if !utils.KeyExists(fields.OptionsHeaderMap, section.Header.Name) { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: fmt.Sprintf("Unknown section '%s'. It must be one of: [Interface], [Peer]", section.Header.Name), + Range: section.Header.ToLSPRange(), + Severity: &common.SeverityError, + }) + // Do not check as the section is unknown + checkAllowedProperty = false + } + + if len(section.Properties) == 0 { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "This section is empty", + Range: section.Header.ToLSPRange(), + Severity: &common.SeverityInformation, + Tags: []protocol.DiagnosticTag{ + protocol.DiagnosticTagUnnecessary, + }, + }) + } else { + existingProperties := make(map[string]*ast.WGProperty) + + for _, property := range section.Properties { + if property.Key.Name == "" { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "This property is missing a name", + Range: property.Key.ToLSPRange(), + Severity: &common.SeverityError, + }) + } + + if property.Value == nil || property.Value.Value == "" { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "This property is missing a value", + Range: property.Value.ToLSPRange(), + Severity: &common.SeverityError, + }) + } + + if checkAllowedProperty { + options := fields.OptionsHeaderMap[section.Header.Name] + + if !utils.KeyExists(options, property.Key.Name) { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: fmt.Sprintf("Unknown property '%s'", property.Key.Name), + Range: property.Key.ToLSPRange(), + Severity: &common.SeverityError, + }) + } else if existingProperty, found := existingProperties[property.Key.Name]; found { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: fmt.Sprintf("Property '%s' has already been defined on line %d", property.Key.Name, existingProperty.Start.Line+1), + Severity: &common.SeverityError, + Range: existingProperty.ToLSPRange(), + }) + } + } + } + } + } +}