feat: funciones NordVPN bash y Go — CLI, contenedor Docker y parser de estado
Funciones bash para instalar, conectar, desconectar, estado, IP, ciudades, países y protocolo. Funciones Go para gestionar contenedor NordVPN (run/start/stop) y parsear estado. Incluye tipo NordVPNStatus y tests para el parser. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// NordVPNContainerRunOpts opciones para ejecutar un container a traves del gateway NordVPN.
|
||||
type NordVPNContainerRunOpts struct {
|
||||
Image string // Imagen Docker a ejecutar (obligatorio)
|
||||
Cmd []string // Comando a ejecutar en el container
|
||||
Env map[string]string // Variables de entorno
|
||||
Volumes []string // Bind mounts
|
||||
Name string // Nombre del container (opcional)
|
||||
Gateway string // Nombre del container NordVPN gateway (default: "nordvpn")
|
||||
Detach bool // Ejecutar en background
|
||||
Remove bool // Eliminar al terminar (--rm)
|
||||
}
|
||||
|
||||
// NordVPNContainerRun ejecuta un container Docker cuyo trafico de red
|
||||
// pasa por el container gateway NordVPN usando --network=container:<gateway>.
|
||||
// Devuelve el ID del container creado.
|
||||
func NordVPNContainerRun(opts NordVPNContainerRunOpts) (string, error) {
|
||||
if opts.Image == "" {
|
||||
return "", fmt.Errorf("image required")
|
||||
}
|
||||
if opts.Gateway == "" {
|
||||
opts.Gateway = "nordvpn"
|
||||
}
|
||||
|
||||
id, err := DockerRunContainer(opts.Image, DockerRunOpts{
|
||||
Name: opts.Name,
|
||||
Env: opts.Env,
|
||||
Volumes: opts.Volumes,
|
||||
Detach: opts.Detach,
|
||||
Remove: opts.Remove,
|
||||
Network: "container:" + opts.Gateway,
|
||||
Cmd: opts.Cmd,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("nordvpn container run %s: %w", opts.Image, err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: nordvpn_container_run
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func NordVPNContainerRun(opts NordVPNContainerRunOpts) (string, error)"
|
||||
description: "Ejecuta un container Docker cuyo trafico pasa por el gateway NordVPN usando --network=container:<gateway>. El container hereda la IP y tunel VPN del gateway."
|
||||
tags: [vpn, nordvpn, docker, container, run, infra, network]
|
||||
uses_functions: ["docker_run_container_go_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/nordvpn_container_run.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Verificar IP desde VPN
|
||||
id, err := NordVPNContainerRun(NordVPNContainerRunOpts{
|
||||
Image: "curlimages/curl",
|
||||
Cmd: []string{"https://api.ipify.org"},
|
||||
Remove: true,
|
||||
Gateway: "nordvpn",
|
||||
})
|
||||
|
||||
// Ejecutar scraper bajo VPN
|
||||
id, err := NordVPNContainerRun(NordVPNContainerRunOpts{
|
||||
Image: "my-scraper:latest",
|
||||
Env: map[string]string{"TARGET_URL": "https://example.com"},
|
||||
Volumes: []string{"/tmp/output:/output"},
|
||||
Detach: true,
|
||||
Name: "scraper-vpn",
|
||||
})
|
||||
|
||||
// Navegador headless bajo VPN
|
||||
id, err := NordVPNContainerRun(NordVPNContainerRunOpts{
|
||||
Image: "chromedp/headless-shell",
|
||||
Detach: true,
|
||||
Name: "chrome-vpn",
|
||||
})
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Requiere que el container gateway NordVPN este corriendo (usar `NordVPNContainerStart` primero). El container cliente no necesita capabilities especiales — hereda la red del gateway. Con `--network=container:X` el container no puede exponer puertos propios; los puertos deben mapearse en el gateway.
|
||||
@@ -0,0 +1,73 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NordVPNContainerOpts opciones para el container gateway NordVPN.
|
||||
type NordVPNContainerOpts struct {
|
||||
Token string // Token de acceso NordVPN (obligatorio)
|
||||
Country string // Pais al que conectar (opcional, ej: "Spain")
|
||||
City string // Ciudad (opcional, ej: "Madrid")
|
||||
Protocol string // "NordLynx" o "OpenVPN" (default: NordLynx)
|
||||
Name string // Nombre del container (default: "nordvpn")
|
||||
}
|
||||
|
||||
// NordVPNContainerStart levanta un container Docker con NordVPN como gateway.
|
||||
// Otros containers pueden usar su red con --network=container:<name>.
|
||||
// Espera hasta que el tunel este activo o timeout de 30s.
|
||||
func NordVPNContainerStart(opts NordVPNContainerOpts) (string, error) {
|
||||
if opts.Token == "" {
|
||||
return "", fmt.Errorf("nordvpn token required")
|
||||
}
|
||||
if opts.Name == "" {
|
||||
opts.Name = "nordvpn"
|
||||
}
|
||||
if opts.Protocol == "" {
|
||||
opts.Protocol = "NordLynx"
|
||||
}
|
||||
|
||||
env := map[string]string{
|
||||
"TOKEN": opts.Token,
|
||||
"TECHNOLOGY": opts.Protocol,
|
||||
}
|
||||
if opts.Country != "" {
|
||||
connect := opts.Country
|
||||
if opts.City != "" {
|
||||
connect += " " + opts.City
|
||||
}
|
||||
env["CONNECT"] = connect
|
||||
}
|
||||
|
||||
// Limpiar container previo con el mismo nombre si existe
|
||||
_ = DockerRemoveContainer(opts.Name, true)
|
||||
|
||||
id, err := DockerRunContainer("ghcr.io/bubuntux/nordvpn", DockerRunOpts{
|
||||
Name: opts.Name,
|
||||
Env: env,
|
||||
Detach: true,
|
||||
CapAdd: []string{"NET_ADMIN", "NET_RAW"},
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("nordvpn container start: %w", err)
|
||||
}
|
||||
|
||||
// Esperar a que el tunel este activo
|
||||
for i := 0; i < 30; i++ {
|
||||
time.Sleep(1 * time.Second)
|
||||
logs, logErr := DockerContainerLogs(opts.Name, 20)
|
||||
if logErr != nil {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(logs, "Connected") || strings.Contains(logs, "connected") {
|
||||
return id, nil
|
||||
}
|
||||
if strings.Contains(logs, "error") || strings.Contains(logs, "failed") {
|
||||
return id, fmt.Errorf("nordvpn connection failed, check logs: docker logs %s", opts.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return id, fmt.Errorf("nordvpn connection timeout after 30s, check logs: docker logs %s", opts.Name)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: nordvpn_container_start
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func NordVPNContainerStart(opts NordVPNContainerOpts) (string, error)"
|
||||
description: "Levanta un container Docker con NordVPN como gateway de red. Otros containers pueden rutear su trafico a traves de este con --network=container:<name>. Espera hasta 30s a que el tunel este activo."
|
||||
tags: [vpn, nordvpn, docker, container, gateway, infra, network]
|
||||
uses_functions: ["docker_run_container_go_infra", "docker_remove_container_go_infra", "docker_container_logs_go_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt, strings, time]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/nordvpn_container_start.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
id, err := NordVPNContainerStart(NordVPNContainerOpts{
|
||||
Token: os.Getenv("NORDVPN_TOKEN"),
|
||||
Country: "Spain",
|
||||
City: "Madrid",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("VPN gateway:", id)
|
||||
|
||||
// Ahora otros containers pueden usar la VPN:
|
||||
// docker run --network=container:nordvpn curlimages/curl https://api.ipify.org
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Usa la imagen `ghcr.io/bubuntux/nordvpn`. Requiere un token de acceso NordVPN (obtener con `nordvpn token` desde CLI o desde la web de NordVPN). Limpia containers previos con el mismo nombre automaticamente. El protocolo por defecto es NordLynx (WireGuard).
|
||||
@@ -0,0 +1,29 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// NordVPNContainerStop detiene y elimina el container gateway NordVPN.
|
||||
// Tambien detiene containers que usen su red si se proporcionan.
|
||||
func NordVPNContainerStop(gateway string, clientNames ...string) error {
|
||||
if gateway == "" {
|
||||
gateway = "nordvpn"
|
||||
}
|
||||
|
||||
// Primero parar los clientes que usan la red del gateway
|
||||
for _, name := range clientNames {
|
||||
_ = DockerStopContainer(name, 5)
|
||||
_ = DockerRemoveContainer(name, true)
|
||||
}
|
||||
|
||||
// Parar y eliminar el gateway
|
||||
if err := DockerStopContainer(gateway, 10); err != nil {
|
||||
return fmt.Errorf("nordvpn container stop: %w", err)
|
||||
}
|
||||
if err := DockerRemoveContainer(gateway, true); err != nil {
|
||||
return fmt.Errorf("nordvpn container remove: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: nordvpn_container_stop
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func NordVPNContainerStop(gateway string, clientNames ...string) error"
|
||||
description: "Detiene y elimina el container gateway NordVPN y opcionalmente los containers cliente que usan su red."
|
||||
tags: [vpn, nordvpn, docker, container, stop, cleanup, infra]
|
||||
uses_functions: ["docker_stop_container_go_infra", "docker_remove_container_go_infra"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [fmt]
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/nordvpn_container_stop.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
// Parar solo el gateway
|
||||
err := NordVPNContainerStop("nordvpn")
|
||||
|
||||
// Parar gateway y clientes asociados
|
||||
err := NordVPNContainerStop("nordvpn", "chrome-vpn", "scraper-vpn")
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Detiene primero los containers cliente (si se proporcionan) y luego el gateway. Importante: si los clientes usan `--network=container:nordvpn`, deben pararse antes que el gateway para evitar errores de red. Los clientes se paran con timeout de 5s, el gateway con 10s.
|
||||
@@ -0,0 +1,68 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NordVPNStatus representa el estado parseado de nordvpn status.
|
||||
type NordVPNStatus struct {
|
||||
Connected bool // true si hay conexion activa
|
||||
Status string // "Connected" o "Disconnected"
|
||||
Hostname string // ej: "es42.nordvpn.com"
|
||||
IP string // IP del servidor VPN
|
||||
Country string // ej: "Spain"
|
||||
City string // ej: "Madrid"
|
||||
Technology string // ej: "NordLynx"
|
||||
Protocol string // ej: "nordlynx"
|
||||
Transfer string // ej: "1.2 MiB received, 500 KiB sent"
|
||||
Uptime string // ej: "5 minutes 32 seconds"
|
||||
}
|
||||
|
||||
// ansiRegexp elimina codigos de escape ANSI.
|
||||
var ansiRegexp = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
|
||||
// ParseNordVPNStatus parsea la salida de texto de `nordvpn status`
|
||||
// a un struct tipado. Funcion pura — no ejecuta comandos.
|
||||
func ParseNordVPNStatus(output string) NordVPNStatus {
|
||||
var s NordVPNStatus
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
for _, line := range lines {
|
||||
line = ansiRegexp.ReplaceAllString(line, "")
|
||||
line = strings.TrimSpace(line)
|
||||
line = strings.TrimLeft(line, "- ")
|
||||
|
||||
idx := strings.Index(line, ":")
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
key := strings.ToLower(strings.TrimSpace(line[:idx]))
|
||||
val := strings.TrimSpace(line[idx+1:])
|
||||
|
||||
switch key {
|
||||
case "status":
|
||||
s.Status = val
|
||||
s.Connected = strings.EqualFold(val, "connected")
|
||||
case "hostname", "server":
|
||||
s.Hostname = val
|
||||
case "ip":
|
||||
s.IP = val
|
||||
case "country":
|
||||
s.Country = val
|
||||
case "city":
|
||||
s.City = val
|
||||
case "current technology":
|
||||
s.Technology = val
|
||||
case "current protocol":
|
||||
s.Protocol = val
|
||||
case "transfer":
|
||||
s.Transfer = val
|
||||
case "uptime":
|
||||
s.Uptime = val
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
name: parse_nordvpn_status
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "func ParseNordVPNStatus(output string) NordVPNStatus"
|
||||
description: "Parsea la salida de texto de nordvpn status a un struct tipado. Elimina codigos ANSI y normaliza claves."
|
||||
tags: [vpn, nordvpn, parser, pure, infra]
|
||||
uses_functions: []
|
||||
uses_types: ["NordVPNStatus_go_infra"]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: ["regexp", "strings"]
|
||||
tested: true
|
||||
tests: ["TestParseNordVPNStatus_Connected", "TestParseNordVPNStatus_Disconnected", "TestParseNordVPNStatus_WithANSI"]
|
||||
test_file_path: "functions/infra/parse_nordvpn_status_test.go"
|
||||
file_path: "functions/infra/parse_nordvpn_status.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
output := `Status: Connected
|
||||
Hostname: es42.nordvpn.com
|
||||
IP: 185.230.124.42
|
||||
Country: Spain
|
||||
City: Madrid
|
||||
Current Technology: NordLynx`
|
||||
|
||||
s := ParseNordVPNStatus(output)
|
||||
// s.Connected == true
|
||||
// s.Hostname == "es42.nordvpn.com"
|
||||
// s.Country == "Spain"
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
Funcion pura — no ejecuta comandos ni accede a red. Maneja codigos ANSI que NordVPN CLI emite en terminal. Campos no presentes en la salida quedan como zero value.
|
||||
@@ -0,0 +1,62 @@
|
||||
package infra
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseNordVPNStatus_Connected(t *testing.T) {
|
||||
input := `Status: Connected
|
||||
Hostname: es42.nordvpn.com
|
||||
IP: 185.230.124.42
|
||||
Country: Spain
|
||||
City: Madrid
|
||||
Current Technology: NordLynx
|
||||
Current Protocol: nordlynx
|
||||
Transfer: 1.2 MiB received, 500 KiB sent
|
||||
Uptime: 5 minutes 32 seconds`
|
||||
|
||||
got := ParseNordVPNStatus(input)
|
||||
|
||||
if !got.Connected {
|
||||
t.Error("expected Connected=true")
|
||||
}
|
||||
if got.Hostname != "es42.nordvpn.com" {
|
||||
t.Errorf("Hostname = %q, want es42.nordvpn.com", got.Hostname)
|
||||
}
|
||||
if got.IP != "185.230.124.42" {
|
||||
t.Errorf("IP = %q, want 185.230.124.42", got.IP)
|
||||
}
|
||||
if got.Country != "Spain" {
|
||||
t.Errorf("Country = %q, want Spain", got.Country)
|
||||
}
|
||||
if got.City != "Madrid" {
|
||||
t.Errorf("City = %q, want Madrid", got.City)
|
||||
}
|
||||
if got.Technology != "NordLynx" {
|
||||
t.Errorf("Technology = %q, want NordLynx", got.Technology)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNordVPNStatus_Disconnected(t *testing.T) {
|
||||
input := `Status: Disconnected`
|
||||
|
||||
got := ParseNordVPNStatus(input)
|
||||
|
||||
if got.Connected {
|
||||
t.Error("expected Connected=false")
|
||||
}
|
||||
if got.Status != "Disconnected" {
|
||||
t.Errorf("Status = %q, want Disconnected", got.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNordVPNStatus_WithANSI(t *testing.T) {
|
||||
input := "\x1b[32mStatus: Connected\x1b[0m\n\x1b[32m- Hostname: us1234.nordvpn.com\x1b[0m"
|
||||
|
||||
got := ParseNordVPNStatus(input)
|
||||
|
||||
if !got.Connected {
|
||||
t.Error("expected Connected=true with ANSI codes")
|
||||
}
|
||||
if got.Hostname != "us1234.nordvpn.com" {
|
||||
t.Errorf("Hostname = %q, want us1234.nordvpn.com", got.Hostname)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user