feat(infra): auto-commit con 86 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user