refactor(server): Improve Wireguard analyzer

Signed-off-by: Myzel394 <github.7a2op@simplelogin.co>
This commit is contained in:
Myzel394 2025-02-23 21:51:45 +01:00 committed by Myzel394
parent 36950fe271
commit eb076dbf53
No known key found for this signature in database
GPG Key ID: B603E877F73D4ABB
5 changed files with 339 additions and 0 deletions

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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,
)
}
}
}

View File

@ -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(),
})
}
}

View File

@ -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(),
})
}
}
}
}
}
}