Files
egutierrez 621e8895c9 feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:38:15 +02:00

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