Metadata-Version: 2.4
Name: upnpDDNS
Version: 3.0.2
Summary: Agente del servidor MiVPN: abre puertos por UPnP, actualiza el DDNS y gestiona los peers WireGuard desde mivpn.net (Linux y Windows).
Author: Rabi Ouallam Jamladi
Author-email: rabaixa@gmail.com
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Microsoft :: Windows
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: upnpclient
Requires-Dist: PyYAML
Requires-Dist: requests
Requires-Dist: miniupnpc
Requires-Dist: websockets>=10
Requires-Dist: cryptocode
Requires-Dist: psutil
Provides-Extra: keys
Requires-Dist: cryptography>=41; extra == "keys"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# upnpDDNS

Agente del **servidor VPN de MiVPN**. Corre en el equipo que hace de servidor
WireGuard y lo mantiene conectado con `mivpn.net`:

| Comando  | Que hace |
|----------|----------|
| `upnp`   | Abre en el router (UPnP) los puertos del YAML y los renueva cada 2 min. |
| `notify` | Dos websockets con el backend: envia el **estado** (clientes conectados, sistema, IP) y recibe **ordenes** (crear/borrar clientes, activar puertos, enviar/subir el `.conf`). |
| `DDNS`   | Actualiza la IP en el DynDNS (dynv6). |

Funciona en:

- **Linux** (Debian/Ubuntu): con **PiVPN** (imagen Orange Pi original) o con
  **WireGuard nativo** (`wg` / `wg-quick`).
- **Windows**: con **WireGuard for Windows** (`wg.exe` / `wireguard.exe`).
  Se usa embebido en la app de escritorio `wireguard-manager`.

## Instalacion

### Linux sin pantalla (VPS Ubuntu, Raspberry Pi, Orange Pi)

Un instalador y un comando. Es la via recomendada: en un equipo sin escritorio
la app grafica no pinta nada, y lo que se instala aqui es el servidor.

```bash
git clone git@github.com:rabaixo/upnpDDNS.git
cd upnpDDNS
sudo ./deploy/linux/install.sh

sudo mivpn setup -e usuario@correo.com   # vincular con tu cuenta
sudo mivpn enable                        # arrancar, y hacerlo al encender
mivpn status                             # comprobar que sirve
```

El instalador pone las dependencias (`python3-venv`, `wireguard-tools`,
`iptables`), crea un entorno propio en `/opt/mivpn` —el Python del sistema no
se toca, que en Debian 12 y Raspberry Pi OS ni deja (PEP 668)—, deja el
comando en `/usr/local/bin/mivpn` y escribe las dos unidades de systemd
apuntando a ese entorno. Los datos y las claves van a `/etc/mivpn`, con
permisos solo para root.

Ordenes de `mivpn`:

| Orden | Que hace |
|---|---|
| `mivpn setup -e correo` | Vincula el equipo con tu cuenta. Es `mivpn-setup`, que sigue existiendo. |
| `mivpn start` / `stop` / `restart` | Arranca y para el agente. |
| `mivpn enable` / `disable` | Arrancar (o no) con el sistema. `enable` lo deja ademas funcionando. |
| `mivpn status` | Cuenta, servicios, tunel, clientes y modo de conexion. |
| `mivpn logs -f` | El registro de las dos unidades, en vivo. |

`status` devuelve codigo de salida, para encadenarlo o vigilarlo desde un
cron: `0` sirviendo, `1` sin vincular, `2` vinculado pero parado, `3` systemd
no manda aqui (contenedor, WSL). Con `--quick` no consulta la IP publica, asi
que no toca la red.

#### La contrasena

`mivpn setup` inicia sesion en mivpn.net: hace falta para saber de quien es
este equipo. La pide por teclado y no la muestra, y **no la guarda en ningun
sitio**: lo que queda en `/etc/mivpn/agent.yaml` es el token del equipo, que es
lo que usa el agente a partir de entonces. Si desvinculas el equipo, ese token
deja de valer.

Para una instalacion desatendida hay dos vias mas, por orden de preferencia:

```bash
# 1. por la entrada estandar, que no deja rastro
echo "$CLAVE" | sudo mivpn setup -e tu@correo.com --password-stdin

# 2. por el entorno
sudo MIVPN_PASSWORD="$CLAVE" mivpn setup -e tu@correo.com
```

`-p contrasena` sigue existiendo, pero no lo uses en un servidor compartido:
queda en `~/.bash_history` y cualquier usuario del equipo la ve con `ps aux`
mientras dura la orden.

### Linux, a mano

```bash
sudo apt install python3-pip python3-venv wireguard-tools libxslt1-dev
sudo python3 -m venv /opt/mivpn-agent
sudo /opt/mivpn-agent/bin/pip install "git+ssh://git@github.com/rabaixo/upnpDDNS.git@cross-platform"
sudo ln -sf /opt/mivpn-agent/bin/{upnp,notify,DDNS,mivpn-setup,mivpn} /usr/local/bin/

# Vincular este equipo con la cuenta de mivpn.net (pide la contrasena). Si no
# hay servidor WireGuard en /etc/wireguard/wg0.conf, lo crea: clave, red
# 10.8.0.1/24, puerto, y NAT y reenvio en PostUp.
sudo mivpn setup -e usuario@correo.com
sudo cp deploy/linux/*.service /etc/systemd/system/
# Arranca el tunel (wg-quick@wg0) y el agente, y que lo hagan al encender
sudo mivpn enable
sudo mivpn status         # lo mismo que la ventana de la app
sudo mivpn unlink         # desvincular y dejar el equipo limpio
```

`mivpn-setup` hace lo mismo que la app de Windows: inicia sesion, pregunta al
backend el estado del servidor y, segun el caso, **registra este equipo**
(primera vez), **avisa y pide confirmacion** si el servidor ya esta instalado
en otro equipo apagado (`--replace` para no preguntar), o **rechaza** si el
otro equipo esta en linea o el servidor es un dispositivo hardware. Guarda
las credenciales en `/etc/mivpn/agent.yaml`, de donde `notify` las lee sin
argumentos, y recrea los clientes que la cuenta ya tenia.

### Orange Pi (imagen original de MiVPN, hardware)

No hace falta `config.yaml` ni `mivpn-setup`: se detecta `pivpn` y se usan las
rutas antiguas (`/etc/mivpn_token`, `/opt/UPNP_conf.yaml`,
`/home/orangepi/configs`). `notify` sigue aceptando `-t` y `-e`.

### Windows

```bat
pip install "git+ssh://git@github.com/rabaixo/upnpDDNS.git@cross-platform"
```

Normalmente no se instala a mano: la app `wireguard-manager` lo empaqueta con
PyInstaller y arranca `UpnpService` y `NotifyService` dentro de su proceso
(que ya corre como administrador). Datos en `C:\ProgramData\MiVPN\`.

## Configuracion

Fichero `config.yaml` (Linux `/etc/mivpn/`, Windows `C:\ProgramData\MiVPN\`,
o `MIVPN_CONFIG=<ruta>`). Ver [config.example.yaml](config.example.yaml).
Todas las claves son opcionales.

Claves principales:

```yaml
backend: { url: mivpn.net, verify_ssl: true }
wireguard:
  backend: auto        # auto | pivpn | native
  interface: wg0       # wg-server en Windows
  server_conf: /etc/wireguard/wg0.conf
  configs_dir: /etc/mivpn/clients
smtp: { host: ..., port: 587, user: ..., password: ... }   # opcional
relay: { enabled: true, auto_direct: false }               # modo CGNAT
```

## Cortafuegos del propio equipo

UPnP abre el puerto en el ROUTER, pero el paquete todavia tiene que entrar en la
maquina. En un VPS ni siquiera hay router: el unico filtro es ese. Al vincular
el equipo, `mivpn-setup` y la app abren el puerto de WireGuard con lo que haya:
`ufw`, `firewalld` o el cortafuegos de Windows (`upnpDDNS/firewall.py`).

La regla es una sola, de entrada, para ese puerto UDP, y lleva nombre propio
(`MiVPN WireGuard`) para poder encontrarla y quitarla. No se activa ni se
desactiva ningun cortafuegos, y si ya esta abierto no se duplica nada. Con
`firewall: {manage: false}` en `config.yaml` no se toca nada.

## Modo relay (CGNAT)

Cuando el router no tiene IP publica propia, nadie puede iniciar una conexion
hacia este equipo y abrir puertos por UPnP no sirve de nada. En ese caso el
agente abre un tunel SALIENTE (`wg-relay`) hacia un relay de MiVPN, que le
reserva un puerto UDP publico y lo reenvia hacia aqui. La sesion WireGuard del
cliente sigue terminando en casa y sale a Internet con la IP de casa: el relay
solo reenvia paquetes cifrados.

Quien decide es `relay_mode.RelayCoordinator`. Manda el backend: si ya tiene un
relay asignado, se levanta sin diagnosticar nada. Si dice modo directo, se mira
`diagnose()`:

| Diagnostico | Modo | Por que |
|---|---|---|
| `public_ip_on_host` | directo | La IP publica es de esta maquina (VPS, Ubuntu Server, Windows Server). No hay NAT: tampoco se arranca UPnP. |
| `public_ip_on_router` | directo | Router con IP publica; el puerto se abre por UPnP. |
| `router_wan_private_or_cgnat` | relay | CGNAT: nadie puede iniciar una conexion hacia casa. |
| `wan_public_but_diff_from_external` | relay | Hay otro NAT por encima del router (operador, DS-Lite). |
| `no_upnp_could_not_read_wan` | relay | No se pudo abrir el puerto. Se confirma con una segunda lectura: el descubrimiento UPnP falla a ratos y volver a directo no es automatico. |

El resto del proceso:

* Cuando toca relay se pide con `POST /api/mivpn/relay/enable/`, enviando solo
  la clave PUBLICA del tunel; la privada se queda en `<data_dir>/relay.key`.
* Con los datos del backend se escribe `<data_dir>/wg-relay.conf`, se levanta
  el tunel y se baja el MTU del servidor a 1340 por el doble encapsulado.
* Los `.conf` de clientes que se creen desde ese momento apuntan al puerto del
  relay y llevan `MTU = 1340`, no a la IP de casa (que tras CGNAT no recibe
  nada). El backend web tambien lo reescribe al descargarlo, pero el fichero
  que queda en disco, y el que se envia por correo si hay SMTP, ya es correcto.
* Si el admin mueve el servidor a otro relay, el backend lo avisa por el canal
  de ordenes (`mode: relay`) con los datos dentro, y el agente rehace el tunel
  sin reiniciarse.
* `RelayService` comprueba cada 5 minutos que el tunel responde y lo rehace si
  no. Volver a modo directo no es automatico (ver `auto_direct`).

## Uso desde codigo (app de escritorio)

```python
from upnpDDNS.config import Settings
from upnpDDNS.upnp.upnp import UpnpService
from upnpDDNS.websocket.main import NotifyService, build_context

settings = Settings.load(overrides={"wireguard": {"server_conf": r"C:\ProgramData\MiVPN\wg-server.conf"}})
upnp = UpnpService(settings.upnp_conf).start()
ctx = build_context(device_token, email, ws_token=ws_token, settings=settings, upnp_service=upnp)
notify = NotifyService(ctx).start()
...
notify.stop(); upnp.stop()
```

`ws_token` es el token cifrado que devuelve el backend tras el login
(`/api/mivpn/provisioning/`). Si no se tiene, `build_context` cifra
localmente con la clave de `key_file`.

## Formato del YAML de puertos

```yaml
- Name: VPN
  Protocol: UDP
  ExternalPort: 51820
  InternalPort: 51820
  Active: true
  Description: Vpn server
  NewLeaseDuration: 130
```

## Backend nativo: como gestiona los peers

- El `.conf` del servidor es la fuente de verdad. Cada cliente se anade como
  bloque `[Peer]` con marcadores `### begin nombre` / `### end nombre`.
- Si el tunel esta levantado se aplica en caliente con `wg set` (sin cortar
  las conexiones); si no, se aplicara al arrancarlo.
- El `.conf` del cliente se guarda en `configs_dir/<nombre>.conf` y el
  registro en `configs_dir/clients.json`.
- `connected_users` se obtiene de `wg show <iface> dump` y se envia como lista
  de diccionarios (`Name`, `Remote IP`, `Virtual IP`, `Bytes Received`,
  `Bytes Sent`, `Last Seen`, `connected`).

## Documentacion

- [Identidades, claves y modo relay](docs/identidades-y-relay.md) — las tres
  claves que hay en juego, el camino que hace un cliente por el relay, que
  pasa en cada arranque, como cambiar de ordenador y que comprobar cuando un
  cliente recibe su .conf y no navega.

- [Pruebas antes de produccion](docs/pruebas-antes-de-produccion.md) — lista
  para ir tachando, marcando lo que nunca se ha probado.

## Desarrollo

```bash
pip install -e .
python -m pytest tests
```
