feat(wireguard): Add code action: Create peer like this

This commit is contained in:
Myzel394 2025-03-17 22:10:29 +01:00
parent 3857bd5694
commit 15ce5958da
No known key found for this signature in database
GPG Key ID: B603E877F73D4ABB
13 changed files with 474 additions and 181 deletions

View File

@ -22,7 +22,7 @@ func analyzeDNSPropertyContainsFallback(
interfaceSection := sections[0] interfaceSection := sections[0]
property := interfaceSection.FindFirstPropertyByName("DNS") _, property := interfaceSection.FindFirstPropertyByName("DNS")
if property == nil { if property == nil {
return return
@ -44,7 +44,10 @@ func analyzeKeepAlivePropertyIsSet(
) { ) {
for _, section := range ctx.document.Indexes.SectionsByName["Peer"] { for _, section := range ctx.document.Indexes.SectionsByName["Peer"] {
// If an endpoint is set, then we should only check for the keepalive property // If an endpoint is set, then we should only check for the keepalive property
if section.FindFirstPropertyByName("Endpoint") != nil && section.FindFirstPropertyByName("PersistentKeepalive") == nil { _, endpoint := section.FindFirstPropertyByName("Endpoint")
_, persistentKeepAlive := section.FindFirstPropertyByName("PersistentKeepalive")
if endpoint != nil && persistentKeepAlive == nil {
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ 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", 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, Severity: &common.SeverityHint,
@ -58,11 +61,11 @@ func analyzeSymmetricPropertiesSet(
ctx *analyzerContext, ctx *analyzerContext,
) { ) {
for _, section := range ctx.document.Indexes.SectionsByName["Interface"] { for _, section := range ctx.document.Indexes.SectionsByName["Interface"] {
preUpProperty := section.FindFirstPropertyByName("PreUp") _, preUpProperty := section.FindFirstPropertyByName("PreUp")
preDownProperty := section.FindFirstPropertyByName("PreDown") _, preDownProperty := section.FindFirstPropertyByName("PreDown")
postUpProperty := section.FindFirstPropertyByName("PostUp") _, postUpProperty := section.FindFirstPropertyByName("PostUp")
postDownProperty := section.FindFirstPropertyByName("PostDown") _, postDownProperty := section.FindFirstPropertyByName("PostDown")
if preUpProperty != nil && preDownProperty == nil { if preUpProperty != nil && preDownProperty == nil {
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
@ -94,7 +97,7 @@ func analyzeSymmetricPropertiesSet(
} }
} }
type key int type key uint8
const ( const (
lineKey key = iota lineKey key = iota
@ -109,7 +112,7 @@ func analyzeDuplicateAllowedIPs(
ipHostSet := utils.CreateIPv4HostSet() ipHostSet := utils.CreateIPv4HostSet()
for _, section := range ctx.document.Indexes.SectionsByName["Peer"] { for _, section := range ctx.document.Indexes.SectionsByName["Peer"] {
property := section.FindFirstPropertyByName("AllowedIPs") _, property := section.FindFirstPropertyByName("AllowedIPs")
if property == nil { if property == nil {
continue continue
@ -123,12 +126,15 @@ func analyzeDuplicateAllowedIPs(
} }
if ipContext, _ := ipHostSet.ContainsIP(ipAddress); ipContext != nil { if ipContext, _ := ipHostSet.ContainsIP(ipAddress); ipContext != nil {
definedLine := (*ipContext).Value(lineKey).(uint32) ctxx := *ipContext
definedLineRaw := ctxx.Value(lineKey)
definedLine := definedLineRaw.(uint32)
ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{
Message: fmt.Sprintf("This IP range is already covered on line %d", definedLine+1), Message: fmt.Sprintf("This IP range is already covered on line %d", definedLine+1),
Severity: &common.SeverityError, Severity: &common.SeverityError,
Range: property.ToLSPRange(), Range: property.Value.ToLSPRange(),
}) })
} else { } else {
ipContext := context.WithValue( ipContext := context.WithValue(

View File

@ -35,7 +35,7 @@ type WGHeader struct {
type WGSection struct { type WGSection struct {
common.LocationRange common.LocationRange
Header WGHeader Header WGHeader
// [uint32]WGProperty: line number -> WGProperty // [uint32]*WGProperty: line number -> *WGProperty
Properties *treemap.Map Properties *treemap.Map
} }

View File

@ -10,11 +10,11 @@ func (c *WGConfig) FindSectionByLine(line uint32) *WGSection {
line, line,
func(current *WGSection, target uint32) int { func(current *WGSection, target uint32) int {
if target < current.Start.Line { if target < current.Start.Line {
return -1 return 1
} }
if target > current.End.Line { if target > current.End.Line {
return 1 return -1
} }
return 0 return 0
@ -42,28 +42,17 @@ func (c *WGConfig) FindPropertyByLine(line uint32) *WGProperty {
return nil return nil
} }
func (s *WGSection) FindFirstPropertyByName(name string) *WGProperty { func (s *WGSection) FindFirstPropertyByName(name string) (uint32, *WGProperty) {
it := s.Properties.Iterator() it := s.Properties.Iterator()
for it.Next() { for it.Next() {
line := it.Key().(uint32)
property := it.Value().(*WGProperty) property := it.Value().(*WGProperty)
if property.Key.Name == name { if property.Key.Name == name {
return property return line, property
} }
} }
return nil return 0, nil
}
func (s *WGSection) FindPropertyByName(name string) *WGProperty {
it := s.Properties.Iterator()
for it.Next() {
property := it.Value().(*WGProperty)
if property.Key.Name == name {
return property
}
}
return nil
} }
func (s *WGSection) GetLastProperty() *WGProperty { func (s *WGSection) GetLastProperty() *WGProperty {

View File

@ -1,9 +1,7 @@
package handlers package handlers
import ( import (
"config-lsp/handlers/wireguard"
"config-lsp/handlers/wireguard/ast" "config-lsp/handlers/wireguard/ast"
wgcommands "config-lsp/handlers/wireguard/commands"
protocol "github.com/tliron/glsp/protocol_3_16" protocol "github.com/tliron/glsp/protocol_3_16"
) )
@ -13,6 +11,7 @@ type CodeActionName string
const ( const (
CodeActionGeneratePrivateKey CodeActionName = "generatePrivateKey" CodeActionGeneratePrivateKey CodeActionName = "generatePrivateKey"
CodeActionGeneratePresharedKey CodeActionName = "generatePresharedKey" CodeActionGeneratePresharedKey CodeActionName = "generatePresharedKey"
CodeActionCreatePeer CodeActionName = "createPeer"
) )
type CodeAction interface { type CodeAction interface {
@ -20,105 +19,3 @@ type CodeAction interface {
} }
type CodeActionArgs interface{} type CodeActionArgs interface{}
type CodeActionGeneratePrivateKeyArgs struct {
URI protocol.DocumentUri
Line uint32
}
func CodeActionGeneratePrivateKeyArgsFromArguments(arguments map[string]any) CodeActionGeneratePrivateKeyArgs {
return CodeActionGeneratePrivateKeyArgs{
URI: arguments["URI"].(protocol.DocumentUri),
Line: uint32(arguments["Line"].(float64)),
}
}
func (args CodeActionGeneratePrivateKeyArgs) RunCommand(d *wireguard.WGDocument) (*protocol.ApplyWorkspaceEditParams, error) {
privateKey, err := wgcommands.CreateNewPrivateKey()
if err != nil {
return &protocol.ApplyWorkspaceEditParams{}, err
}
section := d.Config.FindSectionByLine(args.Line)
property := d.Config.FindPropertyByLine(args.Line)
if section == nil || property == nil {
return nil, nil
}
label := "Generate Private Key"
return &protocol.ApplyWorkspaceEditParams{
Label: &label,
Edit: protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
args.URI: {
{
NewText: " " + privateKey,
Range: protocol.Range{
Start: protocol.Position{
Line: property.End.Line,
Character: property.End.Character,
},
End: protocol.Position{
Line: property.End.Line,
Character: property.End.Character,
},
},
},
},
},
},
}, nil
}
type CodeActionGeneratePresharedKeyArgs struct {
URI protocol.DocumentUri
Line uint32
}
func CodeActionGeneratePresharedKeyArgsFromArguments(arguments map[string]any) CodeActionGeneratePresharedKeyArgs {
return CodeActionGeneratePresharedKeyArgs{
URI: arguments["URI"].(protocol.DocumentUri),
Line: uint32(arguments["Line"].(float64)),
}
}
func (args CodeActionGeneratePresharedKeyArgs) RunCommand(d *wireguard.WGDocument) (*protocol.ApplyWorkspaceEditParams, error) {
presharedKey, err := wgcommands.CreatePresharedKey()
if err != nil {
return &protocol.ApplyWorkspaceEditParams{}, err
}
section := d.Config.FindSectionByLine(args.Line)
property := d.Config.FindPropertyByLine(args.Line)
if section == nil || property == nil {
return nil, nil
}
label := "Generate Preshared Key"
return &protocol.ApplyWorkspaceEditParams{
Label: &label,
Edit: protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
args.URI: {
{
NewText: " " + presharedKey,
Range: protocol.Range{
Start: protocol.Position{
Line: property.End.Line,
Character: property.End.Character,
},
End: protocol.Position{
Line: property.End.Line,
Character: property.End.Character,
},
},
},
},
},
},
}, nil
}

View File

@ -0,0 +1,191 @@
package handlers
import (
"config-lsp/common"
"config-lsp/handlers/wireguard"
"config-lsp/handlers/wireguard/ast"
wgcommands "config-lsp/handlers/wireguard/commands"
"fmt"
"net"
protocol "github.com/tliron/glsp/protocol_3_16"
)
type CodeActionCreatePeerArgs struct {
URI protocol.DocumentUri
Line uint32
}
func CodeActionCreatePeerArgsFromArguments(arguments map[string]any) CodeActionCreatePeerArgs {
return CodeActionCreatePeerArgs{
URI: arguments["URI"].(protocol.DocumentUri),
Line: uint32(arguments["Line"].(float64)),
}
}
func (args CodeActionCreatePeerArgs) RunCommand(d *wireguard.WGDocument) (*protocol.ApplyWorkspaceEditParams, error) {
interfaceSection := d.Indexes.SectionsByName["Interface"][0]
section := d.Config.FindSectionByLine(args.Line)
label := fmt.Sprintf("Add Peer based on peer on line %d", args.Line)
newSection := section
// IP Address
ipAddressLine, ipAddress := newSection.FindFirstPropertyByName("AllowedIPs")
_, address := interfaceSection.FindFirstPropertyByName("Address")
if ipAddress != nil && address != nil {
_, network, err := net.ParseCIDR(address.Value.Value)
if err == nil {
newIPAddress := createNewIP(*network, ipAddress.Value.Value)
valueEnd := common.Location{
Line: ipAddress.End.Line,
Character: ipAddress.Value.Start.Character + uint32(len(newIPAddress)) + 1,
}
newSection.Properties.Put(
ipAddressLine,
&ast.WGProperty{
LocationRange: common.LocationRange{
Start: ipAddress.Start,
End: valueEnd,
},
Key: ipAddress.Key,
RawValue: newIPAddress,
Separator: address.Separator,
Value: &ast.WGPropertyValue{
LocationRange: common.LocationRange{
Start: ipAddress.Value.Start,
End: valueEnd,
},
Value: newIPAddress,
},
},
)
}
}
// Preshared Key
presharedKeyLine, presharedKey := newSection.FindFirstPropertyByName("PresharedKey")
if presharedKey != nil {
var newKey string
if wgcommands.AreWireguardToolsAvailable() {
createdKey, err := wgcommands.CreatePresharedKey()
if err == nil {
newKey = createdKey
}
} else {
newKey = "[preshared key]"
}
valueEnd := common.Location{
Line: presharedKey.End.Line,
Character: presharedKey.Value.Start.Character + uint32(len(newKey)) + 1,
}
newSection.Properties.Put(
presharedKeyLine,
&ast.WGProperty{
LocationRange: common.LocationRange{
Start: presharedKey.Start,
End: valueEnd,
},
Key: presharedKey.Key,
RawValue: newKey,
Separator: presharedKey.Separator,
Value: &ast.WGPropertyValue{
LocationRange: common.LocationRange{
Start: presharedKey.Value.Start,
End: valueEnd,
},
Value: newKey,
},
},
)
}
lastProperty := newSection.GetLastProperty()
println("last line")
println(lastProperty.End.Line)
println(fmt.Sprintf("~~~%s~~~", writeSection(*newSection)))
newText := writeSection(*newSection)
return &protocol.ApplyWorkspaceEditParams{
Label: &label,
Edit: protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
args.URI: {
{
Range: protocol.Range{
Start: protocol.Position{
Line: lastProperty.End.Line,
Character: lastProperty.End.Character,
},
End: protocol.Position{
Line: lastProperty.End.Line,
Character: lastProperty.End.Character,
},
},
NewText: newText,
},
},
},
},
}, nil
}
func writeSection(section ast.WGSection) string {
text := "\n\n"
text += fmt.Sprintf("[%s]\n", section.Header.Name)
it := section.Properties.Iterator()
for it.Next() {
property := it.Value().(*ast.WGProperty)
text += fmt.Sprintf("%s = %s\n", property.Key.Name, property.Value.Value)
}
return text
}
// Try incrementing the IP address
func createNewIP(
network net.IPNet,
rawIP string,
) string {
parsedIP, _, err := net.ParseCIDR(rawIP)
parsedIP = parsedIP.To4()
if parsedIP == nil {
// IPv6 is not supported
return ""
}
if err != nil {
return ""
}
lastAddress := uint32(network.IP[0])<<24 | uint32(network.IP[1])<<16 | uint32(network.IP[2])<<8 | uint32(network.IP[3])
networkMask, _ := network.Mask.Size()
for index := range 32 - networkMask {
lastAddress |= 1 << index
}
newIP := uint32(parsedIP[0])<<24 | uint32(parsedIP[1])<<16 | uint32(parsedIP[2])<<8 | uint32(parsedIP[3])
newIP += 1
if newIP >= lastAddress || newIP == 0 {
// The IP is the last one, which can't be used
// or even worse, it did a whole overflow
return ""
}
// Here, we successfully incremented the IP correctly
// Let's return the formatted IP now.
return fmt.Sprintf("%d.%d.%d.%d/32", newIP>>24, newIP>>16&0xFF, newIP>>8&0xFF, newIP&0xFF)
}

View File

@ -0,0 +1,42 @@
package handlers
import (
"net"
"testing"
)
func TestCreateNewIPSimple24Mask(t *testing.T) {
_, network, _ := net.ParseCIDR("10.0.0.0/24")
newIP := createNewIP(*network, "10.0.0.1/32")
if newIP != "10.0.0.2/32" {
t.Errorf("Expected 10.0.0.2/32, got %s", newIP)
}
}
func TestCreateNewIPDoesNotWorkWithLast24Mask(t *testing.T) {
_, network, _ := net.ParseCIDR("10.0.0.0/24")
newIP := createNewIP(*network, "10.0.0.254/32")
if newIP != "" {
t.Errorf("Expected empty string, got %s", newIP)
}
}
func TestCreateNewIPDoesNotWorkWithLast24Mask2(t *testing.T) {
_, network, _ := net.ParseCIDR("10.0.0.0/24")
newIP := createNewIP(*network, "10.0.0.255/32")
if newIP != "" {
t.Errorf("Expected empty string, got %s", newIP)
}
}
func TestCreateNewIPComplex20Mask(t *testing.T) {
_, network, _ := net.ParseCIDR("10.0.0.0/20")
newIP := createNewIP(*network, "10.0.0.255/32")
if newIP != "10.0.1.0/32" {
t.Errorf("Expected 10.0.1.0/32, got %s", newIP)
}
}

View File

@ -0,0 +1,59 @@
package handlers
import (
"config-lsp/handlers/wireguard"
wgcommands "config-lsp/handlers/wireguard/commands"
protocol "github.com/tliron/glsp/protocol_3_16"
)
type CodeActionGeneratePrivateKeyArgs struct {
URI protocol.DocumentUri
Line uint32
}
func CodeActionGeneratePrivateKeyArgsFromArguments(arguments map[string]any) CodeActionGeneratePrivateKeyArgs {
return CodeActionGeneratePrivateKeyArgs{
URI: arguments["URI"].(protocol.DocumentUri),
Line: uint32(arguments["Line"].(float64)),
}
}
func (args CodeActionGeneratePrivateKeyArgs) RunCommand(d *wireguard.WGDocument) (*protocol.ApplyWorkspaceEditParams, error) {
privateKey, err := wgcommands.CreateNewPrivateKey()
if err != nil {
return &protocol.ApplyWorkspaceEditParams{}, err
}
section := d.Config.FindSectionByLine(args.Line)
property := d.Config.FindPropertyByLine(args.Line)
if section == nil || property == nil {
return nil, nil
}
label := "Generate Private Key"
return &protocol.ApplyWorkspaceEditParams{
Label: &label,
Edit: protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
args.URI: {
{
NewText: " " + privateKey,
Range: protocol.Range{
Start: protocol.Position{
Line: property.End.Line,
Character: property.End.Character,
},
End: protocol.Position{
Line: property.End.Line,
Character: property.End.Character,
},
},
},
},
},
},
}, nil
}

View File

@ -0,0 +1,50 @@
package handlers
import (
"config-lsp/handlers/wireguard"
wgcommands "config-lsp/handlers/wireguard/commands"
protocol "github.com/tliron/glsp/protocol_3_16"
)
type CodeActionGeneratePresharedKeyArgs struct {
URI protocol.DocumentUri
Line uint32
}
func CodeActionGeneratePresharedKeyArgsFromArguments(arguments map[string]any) CodeActionGeneratePresharedKeyArgs {
return CodeActionGeneratePresharedKeyArgs{
URI: arguments["URI"].(protocol.DocumentUri),
Line: uint32(arguments["Line"].(float64)),
}
}
func (args CodeActionGeneratePresharedKeyArgs) RunCommand(d *wireguard.WGDocument) (*protocol.ApplyWorkspaceEditParams, error) {
presharedKey, err := wgcommands.CreatePresharedKey()
if err != nil {
return &protocol.ApplyWorkspaceEditParams{}, err
}
section := d.Config.FindSectionByLine(args.Line)
property := d.Config.FindPropertyByLine(args.Line)
if section == nil || property == nil {
return nil, nil
}
label := "Generate Preshared Key"
return &protocol.ApplyWorkspaceEditParams{
Label: &label,
Edit: protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
args.URI: {
{
NewText: presharedKey,
Range: property.Value.ToLSPRange(),
},
},
},
},
}, nil
}

View File

@ -0,0 +1,42 @@
package handlers
import (
"config-lsp/handlers/wireguard"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func GetAddPeerLikeThis(
d *wireguard.WGDocument,
params *protocol.CodeActionParams,
) []protocol.CodeAction {
// First, check if is on peer line
line := params.Range.Start.Line
section := d.Config.FindSectionByLine(line)
// Check if section can be copied
if section == nil || section.Start.Line != line || section.Header.Name != "Peer" {
return nil
}
// Then add option
commandID := "wireguard." + CodeActionCreatePeer
command := protocol.Command{
Title: "Create new Peer based on this one",
Command: string(commandID),
Arguments: []any{
CodeActionCreatePeerArgs{
URI: params.TextDocument.URI,
Line: line,
},
},
}
return []protocol.CodeAction{
{
Title: "Create new Peer based on this one",
Command: &command,
},
}
}

View File

@ -0,0 +1,59 @@
package handlers
import (
"config-lsp/handlers/wireguard"
protocol "github.com/tliron/glsp/protocol_3_16"
)
func GetKeepaliveCodeActions(
d *wireguard.WGDocument,
params *protocol.CodeActionParams,
) []protocol.CodeAction {
line := params.Range.Start.Line
for _, section := range d.Indexes.SectionsByName["Peer"] {
if section.Start.Line >= line && line <= section.End.Line {
_, endpoint := section.FindFirstPropertyByName("Endpoint")
_, persistentKeepAlive := section.FindFirstPropertyByName("PersistentKeepalive")
if endpoint != nil && persistentKeepAlive == nil {
var insertionLine uint32
lastProperty := section.GetLastProperty()
if lastProperty == nil {
insertionLine = section.End.Line
} else {
insertionLine = lastProperty.End.Line + 1
}
return []protocol.CodeAction{
{
Title: "Add PersistentKeepalive",
Edit: &protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
params.TextDocument.URI: {
{
Range: protocol.Range{
Start: protocol.Position{
Line: insertionLine,
Character: 0,
},
End: protocol.Position{
Line: insertionLine,
Character: 0,
},
},
NewText: "PersistentKeepalive = 25\n",
},
},
},
},
},
}
}
}
}
return nil
}

View File

@ -7,55 +7,6 @@ import (
protocol "github.com/tliron/glsp/protocol_3_16" protocol "github.com/tliron/glsp/protocol_3_16"
) )
func GetKeepaliveCodeActions(
d *wireguard.WGDocument,
params *protocol.CodeActionParams,
) []protocol.CodeAction {
line := params.Range.Start.Line
for _, section := range d.Indexes.SectionsByName["Peer"] {
if section.Start.Line >= line && line <= section.End.Line {
if section.FindPropertyByName("Endpoint") != nil && section.FindFirstPropertyByName("PersistentKeepalive") == nil {
var insertionLine uint32
lastProperty := section.GetLastProperty()
if lastProperty == nil {
insertionLine = section.End.Line
} else {
insertionLine = lastProperty.End.Line + 1
}
return []protocol.CodeAction{
{
Title: "Add PersistentKeepalive",
Edit: &protocol.WorkspaceEdit{
Changes: map[protocol.DocumentUri][]protocol.TextEdit{
params.TextDocument.URI: {
{
Range: protocol.Range{
Start: protocol.Position{
Line: insertionLine,
Character: 0,
},
End: protocol.Position{
Line: insertionLine,
Character: 0,
},
},
NewText: "PersistentKeepalive = 25\n",
},
},
},
},
},
}
}
}
}
return nil
}
func GetKeyGenerationCodeActions( func GetKeyGenerationCodeActions(
d *wireguard.WGDocument, d *wireguard.WGDocument,
params *protocol.CodeActionParams, params *protocol.CodeActionParams,

View File

@ -15,6 +15,7 @@ func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionPa
actions = append(actions, handlers.GetKeyGenerationCodeActions(d, params)...) actions = append(actions, handlers.GetKeyGenerationCodeActions(d, params)...)
actions = append(actions, handlers.GetKeepaliveCodeActions(d, params)...) actions = append(actions, handlers.GetKeepaliveCodeActions(d, params)...)
actions = append(actions, handlers.GetAddPeerLikeThis(d, params)...)
if len(actions) > 0 { if len(actions) > 0 {
return actions, nil return actions, nil

View File

@ -24,6 +24,12 @@ func WorkspaceExecuteCommand(context *glsp.Context, params *protocol.ExecuteComm
d := wireguard.DocumentParserMap[args.URI] d := wireguard.DocumentParserMap[args.URI]
return args.RunCommand(d)
case string(handlers.CodeActionCreatePeer):
args := handlers.CodeActionCreatePeerArgsFromArguments(params.Arguments[0].(map[string]any))
d := wireguard.DocumentParserMap[args.URI]
return args.RunCommand(d) return args.RunCommand(d)
} }