621e8895c9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
233 lines
6.6 KiB
Go
233 lines
6.6 KiB
Go
package infra
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// WGPeerRemoveStatus indica el resultado de la operacion de borrado.
|
|
type WGPeerRemoveStatus string
|
|
|
|
const (
|
|
WGPeerRemoveStatusRemoved WGPeerRemoveStatus = "removed"
|
|
WGPeerRemoveStatusNotPresent WGPeerRemoveStatus = "not-present"
|
|
)
|
|
|
|
// WGPeerRemoveResult contiene el resultado de WGPeerRemove.
|
|
type WGPeerRemoveResult struct {
|
|
DeviceID string
|
|
Status WGPeerRemoveStatus
|
|
ConfigPath string
|
|
}
|
|
|
|
// WGPeerRemove elimina el bloque [Peer] asociado a deviceID del archivo configPath
|
|
// buscando el comentario "# DeviceID:<deviceID>" y aplica syncconf en caliente.
|
|
// Es idempotente: si el peer no existe devuelve status=not-present sin error.
|
|
//
|
|
// Formato esperado del config (el comentario puede preceder o estar dentro del bloque):
|
|
//
|
|
// # DeviceID:device-001
|
|
// [Peer]
|
|
// PublicKey = ...
|
|
// AllowedIPs = ...
|
|
func WGPeerRemove(deviceID string, configPath string) (WGPeerRemoveResult, error) {
|
|
result := WGPeerRemoveResult{
|
|
DeviceID: deviceID,
|
|
ConfigPath: configPath,
|
|
}
|
|
|
|
if strings.TrimSpace(deviceID) == "" {
|
|
return result, fmt.Errorf("wg_peer_remove: deviceID cannot be empty")
|
|
}
|
|
if strings.TrimSpace(configPath) == "" {
|
|
return result, fmt.Errorf("wg_peer_remove: configPath cannot be empty")
|
|
}
|
|
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return result, fmt.Errorf("wg_peer_remove: read config %s: %w", configPath, err)
|
|
}
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
marker := fmt.Sprintf("# DeviceID:%s", deviceID)
|
|
|
|
// Localizar el marker.
|
|
markerIdx := -1
|
|
for i, line := range lines {
|
|
if strings.TrimSpace(line) == marker {
|
|
markerIdx = i
|
|
break
|
|
}
|
|
}
|
|
if markerIdx == -1 {
|
|
result.Status = WGPeerRemoveStatusNotPresent
|
|
return result, nil
|
|
}
|
|
|
|
// blockStart: la primera linea del bloque a eliminar.
|
|
// Si el marker precede al [Peer], el bloque empieza en el marker.
|
|
// Si el marker esta dentro del [Peer], retrocedemos hasta el [Peer] o su comentario previo.
|
|
blockStart := markerIdx
|
|
|
|
for j := markerIdx - 1; j >= 0; j-- {
|
|
t := strings.TrimSpace(lines[j])
|
|
if t == "[Peer]" {
|
|
blockStart = j
|
|
// Incluir tambien el comentario # DeviceID que preceda a ese [Peer].
|
|
if j > 0 && strings.HasPrefix(strings.TrimSpace(lines[j-1]), "# DeviceID:") {
|
|
blockStart = j - 1
|
|
}
|
|
break
|
|
}
|
|
if strings.HasPrefix(t, "[") && t != "" {
|
|
// Llegamos a otra seccion — el marker precede al [Peer].
|
|
break
|
|
}
|
|
}
|
|
|
|
// blockEnd: primera linea que abre el SIGUIENTE bloque tras el [Peer] actual.
|
|
// Saltamos hasta pasar el [Peer] propio antes de buscar otra seccion.
|
|
blockEnd := len(lines)
|
|
pastOwnPeer := false
|
|
for i := blockStart; i < len(lines); i++ {
|
|
t := strings.TrimSpace(lines[i])
|
|
if !pastOwnPeer {
|
|
if t == "[Peer]" {
|
|
pastOwnPeer = true
|
|
}
|
|
continue
|
|
}
|
|
if (strings.HasPrefix(t, "[") && t != "") || strings.HasPrefix(t, "# DeviceID:") {
|
|
blockEnd = i
|
|
break
|
|
}
|
|
}
|
|
|
|
// Reconstruir: segmento antes del bloque + segmento desde blockEnd.
|
|
before := lines[:blockStart]
|
|
after := lines[blockEnd:]
|
|
|
|
// Quitar lineas vacias finales del segmento anterior.
|
|
for len(before) > 0 && strings.TrimSpace(before[len(before)-1]) == "" {
|
|
before = before[:len(before)-1]
|
|
}
|
|
|
|
var newLines []string
|
|
newLines = append(newLines, before...)
|
|
if len(after) > 0 {
|
|
newLines = append(newLines, "")
|
|
}
|
|
newLines = append(newLines, after...)
|
|
|
|
newContent := strings.TrimRight(strings.Join(newLines, "\n"), "\n") + "\n"
|
|
|
|
if err := os.WriteFile(configPath, []byte(newContent), 0600); err != nil {
|
|
return result, fmt.Errorf("wg_peer_remove: write config %s: %w", configPath, err)
|
|
}
|
|
|
|
// Aplicar syncconf en caliente.
|
|
iface := wgIfaceFromPath(configPath)
|
|
if iface != "" {
|
|
if err := wgSyncConfFn(iface, configPath); err != nil {
|
|
_ = os.WriteFile(configPath, data, 0600)
|
|
return result, fmt.Errorf("wg_peer_remove: syncconf %s: %w", iface, err)
|
|
}
|
|
}
|
|
|
|
result.Status = WGPeerRemoveStatusRemoved
|
|
return result, nil
|
|
}
|
|
|
|
// wgIfaceFromPath extrae el nombre de interfaz del path del config (ej. wg0 de /etc/wireguard/wg0.conf).
|
|
func wgIfaceFromPath(configPath string) string {
|
|
parts := strings.Split(configPath, "/")
|
|
if len(parts) == 0 {
|
|
return ""
|
|
}
|
|
name := parts[len(parts)-1]
|
|
name = strings.TrimSuffix(name, ".conf")
|
|
for _, c := range name {
|
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_') {
|
|
return ""
|
|
}
|
|
}
|
|
return name
|
|
}
|
|
|
|
// wgSyncConfFn aplica `wg syncconf <iface> <configPath>` en caliente. Variable
|
|
// para permitir override en tests (no requiere binario `wg`). WG_SKIP_SYNCCONF=1
|
|
// salta la ejecucion (CI sin wg-tools).
|
|
var wgSyncConfFn = func(iface, configPath string) error {
|
|
if os.Getenv("WG_SKIP_SYNCCONF") == "1" {
|
|
return nil
|
|
}
|
|
cmd := exec.Command("wg", "syncconf", iface, configPath)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %s", err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// wgLookupPeerPublicKey busca la PublicKey del peer identificado por deviceID en configPath.
|
|
// El comentario "# DeviceID:<id>" puede preceder al [Peer] o estar dentro del bloque.
|
|
func wgLookupPeerPublicKey(deviceID, configPath string) (string, error) {
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("wg_lookup_peer: read %s: %w", configPath, err)
|
|
}
|
|
|
|
lines := strings.Split(string(data), "\n")
|
|
marker := fmt.Sprintf("# DeviceID:%s", deviceID)
|
|
|
|
markerIdx := -1
|
|
for i, line := range lines {
|
|
if strings.TrimSpace(line) == marker {
|
|
markerIdx = i
|
|
break
|
|
}
|
|
}
|
|
if markerIdx == -1 {
|
|
return "", fmt.Errorf("wg_lookup_peer: peer %s not found in %s", deviceID, configPath)
|
|
}
|
|
|
|
// Buscar PublicKey hacia adelante desde el marker (saltando la linea [Peer]).
|
|
for i := markerIdx + 1; i < len(lines); i++ {
|
|
line := strings.TrimSpace(lines[i])
|
|
if line == "[Peer]" {
|
|
continue
|
|
}
|
|
if (strings.HasPrefix(line, "[") && line != "") || strings.HasPrefix(line, "# DeviceID:") {
|
|
break
|
|
}
|
|
if strings.HasPrefix(line, "PublicKey") {
|
|
if idx := strings.Index(line, " = "); idx != -1 {
|
|
return strings.TrimSpace(line[idx+3:]), nil
|
|
}
|
|
if idx := strings.Index(line, "="); idx != -1 {
|
|
return strings.TrimSpace(line[idx+1:]), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: buscar hacia atras (marker dentro del bloque).
|
|
for i := markerIdx - 1; i >= 0; i-- {
|
|
line := strings.TrimSpace(lines[i])
|
|
if strings.HasPrefix(line, "[") && line != "" {
|
|
break
|
|
}
|
|
if strings.HasPrefix(line, "PublicKey") {
|
|
if idx := strings.Index(line, " = "); idx != -1 {
|
|
return strings.TrimSpace(line[idx+3:]), nil
|
|
}
|
|
if idx := strings.Index(line, "="); idx != -1 {
|
|
return strings.TrimSpace(line[idx+1:]), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("wg_lookup_peer: PublicKey not found for peer %s in %s", deviceID, configPath)
|
|
}
|