diff --git a/.env.example b/.env.example deleted file mode 100644 index a326ba1..0000000 --- a/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# BGP Configuration -BGP_AS=65000 -BIRD_PASSWORD=secret_md5_change_me - -# TINC Configuration -TINC_PORT=655 -TINC_NETNAME=bgpmesh - -# etcd Cluster -ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380 - -# Monitoring -GRAFANA_ADMIN_PASSWORD=admin diff --git a/.gitignore b/.gitignore index f0c92de..0ae123f 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,11 @@ rsa_key.priv hosts/ +# TLS certificates (generated) +# Note: Certificate files in deploy/*/certs/ are tracked for documentation +*.key +*.pem + # IDE .vscode/ .idea/ diff --git a/.playwright-mcp/grafana-dashboard-working.png b/.playwright-mcp/grafana-dashboard-working.png deleted file mode 100644 index e7b72ba..0000000 Binary files a/.playwright-mcp/grafana-dashboard-working.png and /dev/null differ diff --git a/Arquitectura.md b/Arquitectura.md deleted file mode 100644 index 7250378..0000000 --- a/Arquitectura.md +++ /dev/null @@ -1,296 +0,0 @@ -### Introducción a la Estructura de Directorios y Documentación para el Proyecto BGP: Contexto Arquitectónico y Decisiones de Diseño - -La estructura de directorios propuesta para este proyecto BGP overlay sobre TINC mesh se diseña con un enfoque minimalista pero robusto, priorizando modularidad para facilitar el desarrollo local en Sprint 1 (inicialmente 3 nodos) y escalado en Sprint 1.5 a 5 nodos en full mesh (5 nodos cada uno para BIRD, TINC, daemon Go y etcd, con monitoring via Prometheus/Grafana). El "por qué" de esta organización radica en la separación de preocupaciones: root para metadatos globales, docs para conocimiento persistente, docker para isolation de servicios (usando multi-stage builds para reducir image sizes ~20-30% en comparación con single-layer), configs para templates idempotentes (Jinja2 para parametrización dinámica, permitiendo overrides via Ansible vars sin editar archivos base), ansible para orquestación (roles atómicos para reusabilidad en prod scaling), daemon-go para lógica custom de propagación (estructurado en pkgs para testabilidad unitaria con go test -v), ci-cd para automation temprana (GitHub Actions para linting y basic tests, evitando regressions en early commits), y tests para validación end-to-end (bash scripts para simular peering sin dependencias externas pesadas). Trade-offs incluyen mayor nesting en subdirs (e.g., roles/bird/tasks) que aumenta path lengths pero mejora discoverability; limitaciones como potencial para config drifts si vars no se versionan, mitigadas con ansible --diff en Makefile validate. Consideraciones de rendimiento: En dev local (host con >8GB RAM), docker-compose up converge en <2min, con etcd quorum reads <10ms para propagación de peers TINC (keys RSA-2048 via tinc generate-keys). Edge cases: Conflicts en ports (e.g., BIRD 179 sobre TINC tun0); resuelve con networks custom en compose. Best practices: Sigue conventional layouts (e.g., Go src en cmd/pkg, Ansible Galaxy-compatible roles). Alternativas descartadas: Flat structure (pierde modularidad); monorepo con lerna (overkill para single-lang). Si tu host es macOS (con Docker Desktop quirks como slow volumes), ajusta con --platform linux/amd64 en Dockerfiles. Total archivos: 28 exactos, optimizados para quick bootstrap. - -A continuación, detallo cada sección solicitada con precisión técnica, conectando componentes (e.g., tinc-up.j2 inyecta etcd puts para discovery, consumidos por mdns.go en daemon). Esto permite creación inmediata: copia el tree, popula con contenidos esqueleto, y ejecuta make deploy-local para un mesh funcional con BGP sessions over TINC, propagando routes IPv6 /48 con metric tuning en bird.filters.conf. - -## 1. ÁRBOL DE DIRECTORIOS EXACTO - -El árbol se estructura para escalabilidad, con root limpio (solo 6 archivos para quick git clone y overview) y subdirs temáticos. Cada entry incluye propósito, y al final relaciones clave. Usa `tree -a` like format para visualización. - -``` -project-bgp/ -├── .gitignore # Ignora artifacts efímeros como builds Go, env secrets, y Docker caches para mantener repo limpio y seguro; previene commits accidentales de keys TINC o AS BGP. -├── README.md # Overview general del proyecto, enlazando a QUICKSTART y decisions; sirve como entry point para nuevos devs, explicando stack (BIRD 3.x, TINC 1.0, etcd 3.5+). -├── Makefile # Automatiza workflows locales: build, deploy, test; usa GNU Make para portability, con targets paralelizables para speed en CI. -├── docker-compose.yml # Orquesta servicios locales: 5x bird/tinc/daemon/etcd + monitoring (21 containers total en Sprint 1.5); define networks para simular TINC mesh over Docker bridge, volumes para persistencia de etcd data. -├── .env.example # Template para vars sensibles (e.g., BGP passwords, etcd endpoints); evita hardcoding, permitiendo overrides en .env local sin git track. -├── .editorconfig # Estándares de formatting cross-editor (e.g., indent 4 para YAML/Ansible, 8 para Go); asegura consistencia en PRs, reduciendo diffs noise. -├── docs/ # Directorio para documentación no-code; separado de root para evitar clutter, con git submodules potenciales para versioning. -│ ├── QUICKSTART.md # Guía paso-a-paso para setup local; incluye troubleshooting para common fails como TINC NAT issues o BIRD flap debugging. -│ └── architecture/ # Subdir para ADRs; permite expansión a diagrams (e.g., PlantUML) sin polucionar docs root. -│ └── decisions.md # Registro de decisiones arquitectónicas (ADRs); usa template MDR para traceability, cubriendo porqués como BIRD over FRR (menor mem footprint). -├── docker/ # Contiene builds para servicios; separado para easy CI caching de images, con multi-stage para min size (e.g., bird ~100MB). -│ ├── bird/ # Dockerfile y entrypoint para BIRD nodes; integra con configs/bird para runtime templating. -│ │ ├── Dockerfile # Build spec para BIRD container; from bird:3.1.4, adds jinja2 para templating confs. -│ │ └── entrypoint.sh # Startup logic: render templates, start bird -d, expose control socket para monitoring. -│ ├── tinc/ # Similar para TINC; enfocado en mesh setup con tincd 1.0. -│ │ ├── Dockerfile # From debian:12-slim, install tinc 1.0pre (legacy para compat), copy scripts. -│ │ └── entrypoint.sh # Genera keys si no existen, join mesh, exec tinc-up/down. -│ └── monitoring/ # Unificado para Prometheus/Grafana; reduce complexity vs. separate, con shared volume para datasources. -│ ├── Dockerfile # Multi-service: from prom/prometheus + grafana/grafana, usa supervisord. -│ └── entrypoint.sh # Load configs, start services, healthcheck loops. -├── configs/ # Templates y confs estáticas; versionados para idempotencia, usados por Ansible/entrpoints. -│ ├── bird/ # BGP configs; j2 para dynamic (e.g., peers from etcd), plain para static filters. -│ │ ├── bird.conf.j2 # Core BIRD config: router id, imports/exports; params como {{ bgp_as }}. -│ │ ├── filters.conf # Route-maps y policies; static para performance, e.g., prefix-lists para IPv6 /48. -│ │ └── protocols.conf # Peer definitions; static pero overridable via Ansible. -│ ├── tinc/ # TINC mesh configs; j2 para vars como hostname. -│ │ ├── tinc.conf.j2 # Main conf: Mode=switch, Port=655; integra con tinc-up. -│ │ ├── tinc-up.j2 # Script up: ip addr add, etcd put /peers/{{ hostname }}. -│ │ └── tinc-down.j2 # Cleanup: etcd del, ip link down. -│ ├── etcd/ # Init scripts; vacío si en entrypoint, pero incluye etcd.conf si custom. -│ │ └── etcd.conf # Basic cluster config; static para quorum. -│ └── prometheus/ # Monitoring confs; yaml para scrapes. -│ └── prometheus.yml # Scrape jobs: bird exporter, tinc metrics via custom push. -├── ansible/ # Automation dir; estándar Ansible layout para reusabilidad. -│ ├── ansible.cfg # Global settings: e.g., roles_path=roles, retry_files_enabled=false. -│ ├── site.yml # Top-level playbook: incluye roles para bird/tinc. -│ ├── inventory/ # Hosts def; local para dev. -│ │ └── hosts.ini # Groups: [birds], [tincs], localhost. -│ ├── group_vars/ # Vars shared; all.yml para globals. -│ │ └── all.yml # Vars como tinc_netname=bgpmesh, bgp_as=65000. -│ └── roles/ # Atómicos: bird y tinc. -│ ├── bird/ # Role para BIRD install/config. -│ │ └── tasks/ # Tasks dir; main.yml entry. -│ │ └── main.yml # Tasks: apt install bird, template confs, systemctl enable. -│ └── tinc/ # Similar para TINC. -│ └── tasks/ # -│ └── main.yml # Tasks: apt tinc, template tinc.conf, tincd start. -├── daemon-go/ # Go app para custom propagation; estándar GOPATH layout. -│ ├── go.mod # Deps: go 1.21, github.com/hashicorp/mdns v1.0.5, go.etcd.io/etcd/client/v3 v3.5.14. -│ ├── cmd/ # Entry points. -│ │ └── bgp-daemon/ # Main binary dir. -│ │ └── main.go # Run loop: init mdns, watch etcd, propagate peers. -│ ├── pkg/ # Packages reutilizables. -│ │ ├── discovery/ # mDNS logic. -│ │ │ └── mdns.go # Funcs: Lookup over tinc iface, resolve peers. -│ │ └── types/ # Structs. -│ │ └── types.go # Types: Peer struct { IP net.IP, Key string }. -│ └── README.md # Go-specific: build (go build), run flags. -├── .github/ # CI dir; estándar GitHub. -│ └── workflows/ # -│ └── ci.yml # Workflow: on push, jobs para go lint, ansible syntax. -└── tests/ # Integration tests. - └── integration/ # Subdir para org. - └── test_bgp_peering.sh # Script: docker exec birdc show protocols | grep Established. - -``` - -**Relaciones y Dependencias entre Archivos:** -- docker-compose.yml depende de Dockerfiles (build context) y .env.example (vars como ETCD_CLUSTER); volumes mount configs/* para runtime access. -- bird.conf.j2 usa vars de group_vars/all.yml (e.g., {{ bgp_peers }} from etcd watch en daemon-go). -- tinc-up.j2 integra con etcd.conf (puts keys), consumidos por mdns.go para discovery automático. -- Makefile targets (e.g., deploy-local: docker-compose up -d) dependen de docker-compose.yml y Docker dir. -- site.yml incluye roles/bird/tasks/main.yml, que templates configs/bird/*. -- ci.yml runs make test, que ejecuta test_bgp_peering.sh (asserts on docker logs). -- decisions.md referencia choices en Dockerfiles (e.g., debian-slim over alpine por tinc compat). -- Dependencias cíclicas evitadas: Flow unidireccional root -> ansible -> configs -> docker -> daemon-go -> tests. - -## 2. BREAKDOWN ARCHIVO POR ARCHIVO - -### Root Files (6): -- `.gitignore`: Contiene patrones específicos: `*.o` y `bgp-daemon` para Go builds; `.env` para secrets; `/vendor/` si go modules vendor; `*.log` y `/tmp/` para runtime artifacts; `Dockerfile*` no, pero `/build/` si custom; `roles/*/defaults/` no, pero añade `/etcd/data/` para persistencia. Propósito: Previene leaks de keys TINC o BGP auth, manteniendo repo <10MB. -- `README.md`: Secciones: # Project BGP Overlay (overview con stack); ## Setup (link a QUICKSTART); ## Architecture (high-level: TINC L2 mesh -> BIRD BGP sessions -> etcd propagation); ## Contributing (placeholder); ## License (TBD). Incluye badges para CI status. -- `Makefile`: Targets con comandos: `deploy-local: docker-compose up -d --build`; `test: ./tests/integration/test_bgp_peering.sh`; `monitor: open http://localhost:3000`; `clean: docker-compose down -v`; `validate: ansible-playbook site.yml --check --diff`; `help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort`. Usa PHONY para no-file targets. -- `docker-compose.yml`: (Sprint 1.5) Services: bird1-5 (build: ./docker/bird, ports: 179, volumes: ./configs/bird:/etc/bird, networks: mesh-net, environment: NODE_IP=10.0.0.X, NODE_ID=X, TOTAL_NODES=5, BGP_AS=${BGP_AS} para dynamic peer config); tinc1-5 (ports: 655/udp, cap_add: NET_ADMIN, devices: /dev/net/tun, volumes con Subnet declarations en host files para layer 2 ARP resolution); daemon1-5 (Go daemon para peer propagation via etcd); etcd1-5 (5-node cluster quorum); prometheus (build: ./docker/monitoring, ports: 9090, 3000 para Grafana). Total: 21 containers. Networks: mesh-net (bridge), cluster-net (internal). -- `.env.example`: Vars: `BGP_AS=65000` (ej: 65001 para testing); `TINC_PORT=655`; `ETCD_INITIAL_CLUSTER=etcd1=http://etcd1:2379,etcd2=...`; `BIRD_PASSWORD=secret_md5`; `GRAFANA_ADMIN_PASSWORD=admin`. Comenta cada una con uso. -- `.editorconfig`: Reglas: `root = true`; `[*.{yml,yaml}] indent_size=2`; `[*.go] indent_size=8, charset=utf-8`; `[*.sh] end_of_line=lf, indent_size=4`; `[*.j2] indent_size=2`. Asegura Go fmt compliance. - -### Docs (2): -- `QUICKSTART.md`: Outline: # Quickstart; ## Prereqs (Docker 24+, Go 1.21, Ansible 2.16); ## Setup (git clone, cp .env.example .env, make deploy-local); ## Verify (docker ps, birdc -s /var/run/bird.ctl show protocols); ## Troubleshoot (logs, common: tinc NAT fail -> check UDP); ## Teardown (make clean). -- `architecture/decisions.md`: Template ADR: ## ADR-001: BIRD 3.x over 1.6 (Context: Need MP-BGP; Decision: 3.x por RPKI; Consequences: +features, -mem); Inicial: ADR-001 BIRD version, ADR-002 TINC 1.0 legacy, ADR-003 etcd for propagation. - -### Docker (6): -- `bird/Dockerfile`: `FROM birdnetwork/bird:3.1.4 AS base; RUN apt update && apt install -y python3-jinja2; COPY entrypoint.sh /; ENTRYPOINT ["/entrypoint.sh"]`. Multi-stage si adds. -- `bird/entrypoint.sh`: `#!/bin/sh; jinja2 /etc/bird/bird.conf.j2 -D bgp_as=$BGP_AS > /etc/bird/bird.conf; bird -d -c /etc/bird/bird.conf; while true; do sleep 3600; done` (trap SIGTERM birdcl shutdown). -- `tinc/Dockerfile`: `FROM debian:12-slim; RUN apt update && apt install -y tinc=1.0.36-1; COPY entrypoint.sh /; ENTRYPOINT ["/entrypoint.sh"]`. -- `tinc/entrypoint.sh`: `#!/bin/sh; tinc generate-keys 2048; jinja2 /etc/tinc/tinc.conf.j2 -D hostname=$HOSTNAME > /etc/tinc/tinc.conf; tincd -n bgpmesh -d3; exec tinc-up`. -- `monitoring/Dockerfile`: `FROM prom/prometheus:v2.53.1 AS prom; FROM grafana/grafana:11.2.0; COPY --from=prom /bin/prometheus /bin/; COPY entrypoint.sh /; ENTRYPOINT ["/entrypoint.sh"]`. -- `monitoring/entrypoint.sh`: `#!/bin/sh; prometheus --config.file=/etc/prometheus/prometheus.yml & grafana-server --homepath /usr/share/grafana; wait`. - -### Configs (8): -- `bird/bird.conf.j2`: Vars críticas: {{ router_id }}, {{ bgp_as }} (req); opc: {{ listen_port=179 }}. Lógica: protocol kernel { import all; export all; }. -- `bird/filters.conf`: Static: filter export_peers { if net ~ [2001:db8::/48] then accept; reject; }. Crítico: prefix-lists para anti-hijack. -- `bird/protocols.conf.j2`: (Sprint 1.5) Dynamic peers: usa loop Jinja2 con range(1, total_nodes+1) para generar N-1 peers automáticamente basado en vars NODE_IP, NODE_ID, TOTAL_NODES desde docker-compose.yml. Reemplaza protocols.conf estático para escalabilidad. -- `tinc/tinc.conf.j2`: Crítico: {{ Name=hostname }}, {{ Mode=switch }}; opc: {{ Cipher=AES-256-CBC }}. -- `tinc/tinc-up.j2`: `ip link set $INTERFACE up mtu 1400; ip -6 addr add {{ ipv6_prefix }} dev $INTERFACE; etcdctl put /peers/{{ Name }} "$(tinc info)"`. -- `tinc/tinc-down.j2`: `etcdctl del /peers/{{ Name }}; ip link set $INTERFACE down`. -- `etcd/etcd.conf`: Static: listen-client-urls: http://0.0.0.0:2379. -- `prometheus/prometheus.yml`: Scrape: - job_name: bird; static_configs: - targets: ['bird1:9324']. - -### Ansible (6, ajustado a 5 quitando uno innecesario? Espera, 5: cfg, site, hosts.ini, all.yml, main.yml bird, main.yml tinc): -- Estructura roles: bird/tasks/main.yml: - name: Install; apt: name=bird3; - name: Template; template: src=bird.conf.j2 dest=/etc/bird/bird.conf; - name: Enable; systemd: name=bird enabled=yes. -- tinc similar. -- Vars críticas en all.yml: bgp_as: 65000, tinc_netname: bgpmesh. - -### Daemon Go (5): -- Estructura: cmd/main.go entry, pkg/discovery for mDNS, types for structs. -- Interfaces: type Propagator interface { Discover() []Peer; SyncEtcd(Peer) error; }. -- Deps: mod require github.com/hashicorp/mdns v1.0.5; go.etcd.io/etcd/client/v3 v3.5.14. - -### CI/CD (1): -- `ci.yml`: on: push; jobs: lint-go: runs-on: ubuntu-latest; steps: - checkout; - setup-go v5; - go vet ./...; ansible-syntax: ansible-playbook site.yml --syntax-check. - -### Tests (1): -- `test_bgp_peering.sh`: Cases: docker exec bird1 birdc show protocols | grep -q Established; etcdctl get /peers --prefix | wc -l -eq 3; fail if RTT >100ms in ping over tun. - -## 3. CONTENIDO INICIAL - -**Makefile (completo):** -``` -.PHONY: deploy-local test monitor clean validate help - -deploy-local: ## Deploy local environment - docker-compose up -d --build - -test: ## Run integration tests - ./tests/integration/test_bgp_peering.sh - -monitor: ## Open monitoring dashboard - open http://localhost:3000 || xdg-open http://localhost:3000 - -clean: ## Clean up - docker-compose down -v - -validate: ## Validate configs - ansible-playbook site.yml --check --diff -i inventory/hosts.ini - -help: ## Show this help - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -``` - -**docker-compose.yml (esqueleto Sprint 1.5 - 5 nodos):** -``` -version: '3.8' -services: - bird1: - build: ./docker/bird - ports: - "179:179" - volumes: - ./configs/bird:/etc/bird - networks: - mesh-net - environment: - - BGP_AS=${BGP_AS} - - NODE_IP=10.0.0.1 # Sprint 1.5: dynamic peer config - - NODE_ID=1 - - TOTAL_NODES=5 - # bird2, bird3, bird4, bird5 similar (change NODE_IP, NODE_ID) - - tinc1: - build: ./docker/tinc - ports: - "655:655/udp" - cap_add: - NET_ADMIN - devices: - /dev/net/tun - volumes: - ./configs/tinc:/etc/tinc - depends_on: etcd1 - networks: - mesh-net - # tinc2, tinc3, tinc4, tinc5 similar - - daemon1: # Sprint 1.5: Go daemon para peer propagation - build: ./daemon-go - volumes: - /var/run/tinc:/var/run/tinc - networks: - mesh-net - depends_on: - etcd1 - tinc1 - # daemon2-5 similar - - etcd1: - image: quay.io/coreos/etcd:v3.5.14 - command: etcd --name etcd1 --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://etcd1:2379 --initial-advertise-peer-urls http://etcd1:2380 --initial-cluster etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380,etcd5=http://etcd5:2380 - ports: - "2379:2379" - "2380:2380" - volumes: - etcd1-data:/etcd.data - networks: - cluster-net - # etcd2-5 similar (5-node quorum) - - prometheus: - build: ./docker/monitoring - ports: - "9090:9090" - "3000:3000" - volumes: - ./configs/prometheus:/etc/prometheus -networks: - mesh-net: - driver: bridge - cluster-net: - internal: true -volumes: - etcd1-data: - etcd2-data: - etcd3-data: - etcd4-data: - etcd5-data: -``` - -**QUICKSTART.md (esqueleto extenso):** -``` -# Quickstart Guide - -## Prerequisites -- Docker 24+, Compose v2 -- Go 1.21 for daemon -- Ansible 2.16 - -## Setup -1. git clone repo -2. cp .env.example .env; edit BGP_AS=65001 -3. make deploy-local # Builds and starts 5-node mesh (Sprint 1.5: 21 containers) -4. Wait ~90-120s for convergence (5 nodes take longer) - -## Verify -- docker ps | grep up -- docker exec -it bird1 birdc show protocols all | grep Established # BGP up -- docker exec -it tinc1 tinc -n bgpmesh info # Peers connected -- etcdctl --endpoints=http://localhost:2379 get /peers --prefix # Propagated info - -## Troubleshoot -- TINC fail: check logs docker logs tinc1 | grep error; verify UDP 655 open -- BIRD flaps: birdc show route; tune keepalive in protocols.conf -- Etcd quorum: if down, make clean && deploy-local - -## Teardown -make clean -``` - -## 4. ORDEN DE IMPLEMENTACIÓN - -**Paso 1: Archivos Base (Root y Docs, ~8 archivos)** -Crear primero .gitignore, README.md, .env.example, .editorconfig, QUICKSTART.md, decisions.md, Makefile, docker-compose.yml. Por qué: Establecen skeleton para git init y basic setup; sin ellos, no hay workflow (e.g., Makefile para orquestar). Tiempo: 30min. Dependencias: Ninguna. - -**Paso 2: Configuraciones (Configs dir, 8 archivos)** -Poblar configs/bird/*, tinc/*, etcd.conf, prometheus.yml. Por qué: Son el core data para services; permiten templating temprano sin runtime. Integra vars de .env.example. Tiempo: 45min. Depend: .env.example para params. - -**Paso 3: Servicios Docker (Docker dir, 6 archivos)** -Crear Dockerfiles y entrypoints. Por qué: Habilitan build/test local; entrypoints manejan runtime logic como templating. Tiempo: 1h. Depend: Configs para mounts. - -**Paso 4: Ansible (5 archivos)** -ansible.cfg, site.yml, hosts.ini, all.yml, roles/*/tasks/main.yml. Por qué: Automatiza provisioning; tasks template configs. Tiempo: 45min. Depend: Configs j2. - -**Paso 5: Daemon Go (5 archivos)** -go.mod, main.go, mdns.go, types.go, README.md. Por qué: Implementa propagación custom; build con go build. Tiempo: 1h. Depend: Etcd up de Docker. - -**Paso 6: CI/CD y Tests (2 archivos)** -ci.yml, test_bgp_peering.sh. Por qué: Valida todo; ci runs on push. Tiempo: 30min. Depend: Todo anterior. - -## 5. CHECKPOINTS DE VALIDACIÓN - -Después de Paso 1: `git status` clean; `make help` lists targets; `cat QUICKSTART.md` covers basics. Criterio: No errors en make validate (stub inicial). - -Después de Paso 2: `jinja2 configs/bird/bird.conf.j2 -D bgp_as=65000` outputs valid conf; grep critical vars. Criterio: No syntax errors. - -Después de Paso 3: `docker build ./docker/bird` succeeds; `docker-compose up -d` starts without crash; `docker logs bird1` shows bird running. Criterio: Services up >1min sin exits. - -Después de Paso 4: `make validate` passes --check; `ansible-playbook site.yml -i hosts.ini` templates sin diffs. Criterio: Idempotente (second run no changes). - -Después de Paso 5: `cd daemon-go; go mod tidy; go build ./cmd/bgp-daemon; ./bgp-daemon` connects etcd, discovers peers. Criterio: Logs show sync, no panics. - -Después de Paso 6: `make test` passes all cases; push to GitHub triggers ci.yml success. Criterio: 100% assertions true, RTT <200ms. - -## 6. INTERDEPENDENCIAS - -- docker-compose.yml depende de: Dockerfiles (build), .env.example (env vars), configs/* (volumes), monitoring/entrypoint.sh (startup). -- bird.conf.j2 depende de: group_vars/all.yml (Jinja vars como bgp_as), protocols.conf (import static peers). -- tinc-up.j2 depende de: etcd.conf (etcdctl endpoints), tinc.conf.j2 (interface name). -- main.go depende de: mdns.go (discovery funcs), types.go (structs), go.mod (deps). -- test_bgp_peering.sh depende de: docker-compose.yml (exec en containers), ci.yml (runs en job). -- site.yml depende de: roles/*/main.yml (includes), inventory/hosts.ini (targets). -- QUICKSTART.md depende de: Makefile (commands), decisions.md (refs ADRs). -- General: Todo fluye a tests para e2e; cyclical mitigado por build order. - -¿Detalles de tu entorno dev (e.g., OS, si usas podman over docker) para tweaks? diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 2d10870..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,826 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -**BGP Overlay Network over TINC Mesh** - A minimalist but robust 5-node local development environment (Sprint 1.5) that integrates: -- **BIRD 2.x**: BGP routing daemon with dynamic peer configuration -- **TINC 1.0**: Layer 2 mesh VPN (switch mode, RSA-2048, AES-256) -- **etcd 3.5+**: Distributed key-value store for peer propagation -- **Prometheus/Grafana**: Monitoring and metrics -- **Go daemon**: Custom propagation logic (etcd watch, TINC topology management) -- **Ansible**: Orchestration with atomic roles (for production deployment) - -**Total Files**: 29, optimized for quick bootstrap -**Convergence Time**: <2min on hosts with >8GB RAM -**Architecture Focus**: Separation of concerns with idempotent configs, dynamic peer discovery -**Scalability**: Full mesh topology supports any number of nodes (tested with 3-5 nodes) - -## Quick Start - -```bash -# Prerequisites check -docker --version # Need 24+ -go version # Need 1.21+ -ansible --version # Need 2.16+ - -# Setup -cp .env.example .env -make deploy-local # Converges in <2min - -# Verify -make test # Integration tests - -# Monitor -make monitor # Open Grafana at localhost:3000 - -# Cleanup -make clean -``` - -## Project Structure (Exact) - -``` -project-bgp/ -├── .gitignore # Go builds, .env, Docker caches, TINC keys -├── README.md # Overview linking to QUICKSTART, stack description -├── Makefile # Automation: deploy-local, test, monitor, clean, validate, help -├── docker-compose.yml # 15 services: 5×bird/tinc/daemon + 5×etcd + prometheus -├── .env.example # BGP_AS, TINC_PORT, ETCD_INITIAL_CLUSTER, BIRD_PASSWORD -├── .editorconfig # indent_size: 2 (YAML), 8 (Go), 4 (sh) -├── docs/ -│ ├── QUICKSTART.md # Setup steps, verification, troubleshooting -│ └── architecture/ -│ └── decisions.md # ADRs: BIRD 3.x choice, TINC 1.0, etcd rationale -├── docker/ -│ ├── bird/ -│ │ ├── Dockerfile # FROM bird:3.1.4, adds jinja2 -│ │ └── entrypoint.sh # Render templates, start bird -d -│ ├── tinc/ -│ │ ├── Dockerfile # FROM debian:12-slim, install tinc 1.0.36 -│ │ └── entrypoint.sh # Generate keys, join mesh, exec tinc-up -│ └── monitoring/ -│ ├── Dockerfile # Multi-stage: prometheus + grafana -│ └── entrypoint.sh # Start both services, healthcheck loops -├── configs/ -│ ├── bird/ -│ │ ├── bird.conf.j2 # Router ID, BGP AS, protocols (vars: router_id, bgp_as) -│ │ ├── filters.conf # Static route-maps, prefix-lists (anti-hijack) -│ │ ├── protocols.conf.j2 # Dynamic BGP peer template (N-1 peers auto-generated) -│ │ └── protocols.conf # Legacy static config (unused, kept for reference) -│ ├── tinc/ -│ │ ├── tinc.conf.j2 # Mode=switch, Cipher=AES-256 (vars: hostname) -│ │ ├── tinc-up.j2 # ip link up, etcd put /peers/{{Name}} -│ │ └── tinc-down.j2 # etcd del, ip link down -│ ├── etcd/ -│ │ └── etcd.conf # Cluster config (listen-client-urls) -│ └── prometheus/ -│ └── prometheus.yml # Scrape configs: bird:9324, tinc metrics -├── ansible/ -│ ├── ansible.cfg # roles_path=roles, retry_files_enabled=false -│ ├── site.yml # Top-level playbook including roles -│ ├── inventory/ -│ │ └── hosts.ini # Groups: [birds], [tincs], localhost -│ ├── group_vars/ -│ │ └── all.yml # tinc_netname=bgpmesh, bgp_as=65000 -│ └── roles/ -│ ├── bird/ -│ │ └── tasks/ -│ │ └── main.yml # apt install bird3, template, systemctl enable -│ └── tinc/ -│ └── tasks/ -│ └── main.yml # apt tinc, template configs, tincd start -├── daemon-go/ -│ ├── go.mod # go 1.21, hashicorp/mdns v1.0.5, etcd/client v3.5.14 -│ ├── cmd/ -│ │ └── bgp-daemon/ -│ │ └── main.go # Run loop: init mdns, watch etcd, propagate peers -│ ├── pkg/ -│ │ ├── discovery/ -│ │ │ └── mdns.go # Lookup over TINC iface, resolve peers -│ │ └── types/ -│ │ └── types.go # Peer struct {IP net.IP, Key string} -│ └── README.md # Build: go build, run flags -├── .github/ -│ └── workflows/ -│ └── ci.yml # on push: go lint, ansible syntax, make test -└── tests/ - └── integration/ - └── test_bgp_peering.sh # docker exec birdc | grep Established -``` - -## Implementation Order (6 Steps) - -### Step 1: Base Files (Root + Docs) - 30min - -**Files to create (8)**: -1. `.gitignore` - Patterns: `*.o`, `bgp-daemon`, `.env`, `/vendor/`, `*.log`, `/tmp/`, `/etcd/data/` -2. `README.md` - Sections: Overview (stack), Setup (link QUICKSTART), Architecture (high-level), Contributing, License -3. `.env.example` - Vars: `BGP_AS=65000`, `TINC_PORT=655`, `ETCD_INITIAL_CLUSTER=...`, `BIRD_PASSWORD=secret_md5` -4. `.editorconfig` - Rules: `[*.{yml,yaml}] indent_size=2`, `[*.go] indent_size=8`, `[*.sh] indent_size=4` -5. `Makefile` - See complete content below -6. `docker-compose.yml` - See complete content below -7. `docs/QUICKSTART.md` - See template below -8. `docs/architecture/decisions.md` - ADR template - -**Validation**: `git status` clean, `make help` lists targets - -**Makefile content**: -```makefile -.PHONY: deploy-local test monitor clean validate help - -deploy-local: ## Deploy local environment - docker-compose up -d --build - -test: ## Run integration tests - ./tests/integration/test_bgp_peering.sh - -monitor: ## Open monitoring dashboard - open http://localhost:3000 || xdg-open http://localhost:3000 - -clean: ## Clean up - docker-compose down -v - -validate: ## Validate configs - ansible-playbook ansible/site.yml --check --diff -i ansible/inventory/hosts.ini - -help: ## Show this help - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' -``` - -**docker-compose.yml skeleton**: -```yaml -version: '3.8' -services: - bird1: - build: ./docker/bird - ports: ["179:179"] - volumes: ["./configs/bird:/etc/bird"] - networks: [mesh-net] - environment: ["BGP_AS=${BGP_AS}"] - # bird2, bird3 similar - - tinc1: - build: ./docker/tinc - ports: ["655:655/udp"] - cap_add: [NET_ADMIN] - devices: ["/dev/net/tun"] - volumes: ["./configs/tinc:/etc/tinc"] - depends_on: [etcd1] - networks: [mesh-net] - # tinc2, tinc3 similar - - etcd1: - image: quay.io/coreos/etcd:v3.5.14 - command: etcd --name etcd1 --listen-client-urls http://0.0.0.0:2379 --advertise-client-urls http://etcd1:2379 --initial-advertise-peer-urls http://etcd1:2380 --initial-cluster etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380 - ports: ["2379:2379", "2380:2380"] - volumes: ["etcd1-data:/etcd.data"] - networks: [cluster-net] - # etcd2, etcd3 similar - - prometheus: - build: ./docker/monitoring - ports: ["9090:9090", "3000:3000"] - volumes: ["./configs/prometheus:/etc/prometheus"] - -networks: - mesh-net: - driver: bridge - cluster-net: - internal: true - -volumes: - etcd1-data: - etcd2-data: - etcd3-data: -``` - -**QUICKSTART.md template**: -```markdown -# Quickstart Guide - -## Prerequisites -- Docker 24+, Compose v2 -- Go 1.21 for daemon -- Ansible 2.16 - -## Setup -1. git clone repo -2. cp .env.example .env; edit BGP_AS=65001 -3. make deploy-local # Builds and starts 5-node full mesh -4. Wait ~1min for convergence - -## Verify -- docker ps | grep up -- docker exec -it bird1 birdc show protocols all | grep Established -- docker exec -it tinc1 tinc -n bgpmesh info -- etcdctl --endpoints=http://localhost:2379 get /peers --prefix - -## Troubleshoot -- TINC fail: check logs docker logs tinc1 | grep error; verify UDP 655 -- BIRD flaps: birdc show route; tune keepalive in protocols.conf -- Etcd quorum: if down, make clean && deploy-local - -## Teardown -make clean -``` - ---- - -### Step 2: Configurations (configs/ dir) - 45min - -**Files to create (9)**: -1. `configs/bird/bird.conf.j2` - Critical vars: `{{ router_id }}`, `{{ bgp_as }}` (required) -2. `configs/bird/filters.conf` - Static prefix-lists: `if net ~ [2001:db8::/48] then accept; reject;` -3. `configs/bird/protocols.conf.j2` - **Dynamic template**: Generates N-1 BGP peers automatically using `{% for peer_id in range(1, total_nodes + 1) %}` loop with vars: `{{ node_ip }}`, `{{ node_id }}`, `{{ bgp_as }}`, `{{ total_nodes }}` -4. `configs/tinc/tinc.conf.j2` - Critical: `{{ Name=hostname }}`, `{{ Mode=switch }}`; optional: `{{ Cipher=AES-256-CBC }}` -5. `configs/tinc/tinc-up.j2` - `ip link set $INTERFACE up mtu 1400; ip -6 addr add {{ ipv6_prefix }} dev $INTERFACE; etcdctl put /peers/{{ Name }} "$(tinc info)"` -6. `configs/tinc/tinc-down.j2` - `etcdctl del /peers/{{ Name }}; ip link set $INTERFACE down` -7. `configs/etcd/etcd.conf` - Static: `listen-client-urls: http://0.0.0.0:2379` -8. `configs/prometheus/prometheus.yml` - Scrape: `- job_name: bird; static_configs: - targets: ['bird1:9324']` - -**Validation**: `jinja2 configs/bird/bird.conf.j2 -D bgp_as=65000` outputs valid conf - -**Dependencies**: Requires `.env.example` for parameter reference - ---- - -### Step 3: Docker Services (docker/ dir) - 1h - -**Files to create (6)**: -1. `docker/bird/Dockerfile`: -```dockerfile -FROM birdnetwork/bird:3.1.4 AS base -RUN apt update && apt install -y python3-jinja2 -COPY entrypoint.sh / -ENTRYPOINT ["/entrypoint.sh"] -``` - -2. `docker/bird/entrypoint.sh`: -```bash -#!/bin/sh -jinja2 /etc/bird/bird.conf.j2 -D bgp_as=$BGP_AS > /etc/bird/bird.conf -bird -d -c /etc/bird/bird.conf -while true; do sleep 3600; done -``` - -3. `docker/tinc/Dockerfile`: -```dockerfile -FROM debian:12-slim -RUN apt update && apt install -y tinc=1.0.36-1 -COPY entrypoint.sh / -ENTRYPOINT ["/entrypoint.sh"] -``` - -4. `docker/tinc/entrypoint.sh`: -```bash -#!/bin/sh -tinc generate-keys 2048 -jinja2 /etc/tinc/tinc.conf.j2 -D hostname=$HOSTNAME > /etc/tinc/tinc.conf -tincd -n bgpmesh -d3 -exec tinc-up -``` - -5. `docker/monitoring/Dockerfile`: -```dockerfile -FROM prom/prometheus:v2.53.1 AS prom -FROM grafana/grafana:11.2.0 -COPY --from=prom /bin/prometheus /bin/ -COPY entrypoint.sh / -ENTRYPOINT ["/entrypoint.sh"] -``` - -6. `docker/monitoring/entrypoint.sh`: -```bash -#!/bin/sh -prometheus --config.file=/etc/prometheus/prometheus.yml & -grafana-server --homepath /usr/share/grafana -wait -``` - -**Validation**: -- `docker build ./docker/bird` succeeds -- `docker-compose up -d` starts without crash -- `docker logs bird1` shows bird running - -**Dependencies**: Requires `configs/*` for volume mounts - ---- - -### Step 4: Ansible Automation (ansible/ dir) - 45min - -**Files to create (5)**: -1. `ansible/ansible.cfg`: -```ini -[defaults] -roles_path = roles -retry_files_enabled = false -host_key_checking = false -``` - -2. `ansible/site.yml`: -```yaml ---- -- hosts: all - become: yes - roles: - - bird - - tinc -``` - -3. `ansible/inventory/hosts.ini`: -```ini -[birds] -bird1 -bird2 -bird3 - -[tincs] -tinc1 -tinc2 -tinc3 - -[all:vars] -ansible_connection=local -``` - -4. `ansible/group_vars/all.yml`: -```yaml ---- -tinc_netname: bgpmesh -bgp_as: 65000 -router_id: 192.0.2.1 -``` - -5. `ansible/roles/bird/tasks/main.yml`: -```yaml ---- -- name: Install BIRD - apt: - name: bird3 - state: present - -- name: Template bird.conf - template: - src: bird.conf.j2 - dest: /etc/bird/bird.conf - notify: restart bird - -- name: Enable BIRD service - systemd: - name: bird - enabled: yes - state: started -``` - -6. `ansible/roles/tinc/tasks/main.yml` - Similar structure for TINC - -**Validation**: -- `make validate` passes --check -- `ansible-playbook ansible/site.yml -i ansible/inventory/hosts.ini` templates without diffs (idempotent) - -**Dependencies**: Requires `configs/*.j2` for templating - ---- - -### Step 5: Go Daemon (daemon-go/ dir) - 1h - -**Files to create (5)**: -1. `daemon-go/go.mod`: -```go -module bgp-daemon - -go 1.21 - -require ( - github.com/hashicorp/mdns v1.0.5 - go.etcd.io/etcd/client/v3 v3.5.14 -) -``` - -2. `daemon-go/cmd/bgp-daemon/main.go`: -```go -package main - -import ( - "bgp-daemon/pkg/discovery" - "bgp-daemon/pkg/types" - "log" -) - -func main() { - log.Println("Starting BGP daemon...") - peers, err := discovery.LookupPeers("tinc0") - if err != nil { - log.Fatal(err) - } - for _, peer := range peers { - log.Printf("Found peer: %v", peer) - } -} -``` - -3. `daemon-go/pkg/discovery/mdns.go`: -```go -package discovery - -import ( - "bgp-daemon/pkg/types" - "github.com/hashicorp/mdns" -) - -func LookupPeers(iface string) ([]types.Peer, error) { - // mDNS lookup logic over TINC interface - entries := make(chan *mdns.ServiceEntry, 10) - peers := []types.Peer{} - - // Parse entries to Peer structs - for entry := range entries { - peers = append(peers, types.Peer{ - IP: entry.AddrV4, - Key: entry.Info, - }) - } - return peers, nil -} -``` - -4. `daemon-go/pkg/types/types.go`: -```go -package types - -import "net" - -type Peer struct { - IP net.IP - Key string - Endpoint string -} -``` - -5. `daemon-go/README.md`: -```markdown -# BGP Daemon - -## Build -go build -o bgp-daemon ./cmd/bgp-daemon - -## Run -./bgp-daemon -v - -## Flags --iface string TINC interface (default "tinc0") --etcd string etcd endpoints (default "localhost:2379") -``` - -**Validation**: -- `cd daemon-go; go mod tidy; go build ./cmd/bgp-daemon` -- `./bgp-daemon` connects etcd, discovers peers -- Logs show sync, no panics - -**Dependencies**: Requires running etcd from Docker - ---- - -### Step 6: CI/CD & Tests (final 2 files) - 30min - -**Files to create (2)**: -1. `.github/workflows/ci.yml`: -```yaml -name: CI - -on: push - -jobs: - lint-go: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: '1.21' - - run: go vet ./daemon-go/... - - ansible-syntax: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: ansible-playbook ansible/site.yml --syntax-check - - integration: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: make deploy-local - - run: make test -``` - -2. `tests/integration/test_bgp_peering.sh`: -```bash -#!/bin/bash -set -euo pipefail - -echo "Testing BGP sessions..." -docker exec bird1 birdc show protocols | grep -q Established || exit 1 - -echo "Testing etcd propagation..." -PEERS=$(docker exec etcd1 etcdctl get /peers --prefix | wc -l) -[[ $PEERS -eq 3 ]] || exit 1 - -echo "Testing TINC connectivity..." -docker exec tinc1 ping -c 3 -W 2 10.0.0.2 || exit 1 - -RTT=$(docker exec tinc1 ping -c 10 10.0.0.2 | grep 'avg' | awk '{print $4}' | cut -d'/' -f2) -[[ $(echo "$RTT < 100" | bc) -eq 1 ]] || exit 1 - -echo "All tests passed!" -``` - -**Validation**: -- `make test` passes all cases -- Push to GitHub triggers ci.yml success -- 100% assertions true, RTT <200ms - ---- - -## File Interdependencies - -**Critical Flow**: -``` -docker-compose.yml → depends on - ├── Dockerfiles (build context) - ├── .env.example (environment vars) - └── configs/* (volume mounts) - -bird.conf.j2 → uses - ├── group_vars/all.yml (Jinja vars like bgp_as) - └── protocols.conf (import static peers) - -tinc-up.j2 → integrates with - ├── etcd.conf (etcdctl endpoints) - └── tinc.conf.j2 (interface name) - -main.go → depends on - ├── mdns.go (discovery funcs) - ├── types.go (Peer struct) - └── go.mod (dependencies) - -test_bgp_peering.sh → depends on - ├── docker-compose.yml (exec on containers) - └── ci.yml (runs in job) - -site.yml → depends on - ├── roles/*/main.yml (task includes) - └── inventory/hosts.ini (targets) -``` - -**No circular dependencies**: Flow is unidirectional `root → ansible → configs → docker → daemon-go → tests` - -## Common Commands - -### Development Workflow -```bash -# Deploy changes -make deploy-local - -# Watch logs -docker logs -f bird1 -docker logs -f tinc1 - -# Verify BGP -docker exec bird1 birdc show protocols all -docker exec bird1 birdc show route - -# Verify TINC -docker exec tinc1 tinc -n bgpmesh dump nodes -docker exec tinc1 tinc -n bgpmesh dump reachable - -# Check etcd -docker exec etcd1 etcdctl get /peers --prefix - -# Restart service -docker restart bird1 -``` - -### Testing -```bash -# Full test suite -make test - -# Individual checks -docker exec bird1 birdc show protocols | grep Established -docker exec tinc1 ping -c 3 10.0.0.2 -docker exec etcd1 etcdctl endpoint health -``` - -### Configuration Changes -```bash -# Edit configs -vim configs/bird/bird.conf.j2 - -# Validate -make validate - -# Apply (restart affected services) -docker restart bird1 bird2 bird3 - -# Verify -docker exec bird1 birdc configure check -``` - -### Go Daemon Development -```bash -cd daemon-go/ - -# Dependencies -go mod tidy - -# Build -go build -o bgp-daemon ./cmd/bgp-daemon - -# Run locally -./bgp-daemon -iface tinc0 -etcd localhost:2379 - -# Test -go test -v ./... - -# Format -go fmt ./... -``` - -### Git Hooks & Pre-Commit Checks - -**Install Pre-Commit Hook** (recommended for all developers): -```bash -# One-time setup -./scripts/install-hooks.sh -``` - -**What the pre-commit hook checks**: -1. ✅ Go code formatting (`gofmt -s`) -2. ✅ Go vet (static analysis) -3. ✅ Unit tests pass - -**Manual pre-commit checks** (if not using hook): -```bash -cd daemon-go/ - -# Check formatting -gofmt -s -l . - -# Fix formatting -gofmt -s -w . - -# Run vet -make vet - -# Run unit tests -make test-unit -``` - -**Skip hook temporarily** (not recommended): -```bash -git commit --no-verify -m "message" -``` - -**Why use pre-commit hooks**: -- Prevents CI failures due to formatting/vet errors -- Catches test failures before pushing -- Saves time by running checks locally first -- Maintains code quality consistently - -## Troubleshooting - -### TINC Not Connecting -```bash -# Check logs -docker logs tinc1 | grep -i error - -# Verify keys generated -docker exec tinc1 ls -la /etc/tinc/bgpmesh/ - -# Check UDP port -docker exec tinc1 netstat -uln | grep 655 - -# Manual connection test -docker exec tinc1 tinc -n bgpmesh add connect tinc2 -``` - -### BIRD Sessions Flapping -```bash -# Check session status -docker exec bird1 birdc show protocols all | grep -A 5 peer1 - -# Verify TINC tunnel stable -docker exec tinc1 ping -c 100 10.0.0.2 - -# Check bird config -docker exec bird1 bird --parse-only -c /etc/bird/bird.conf - -# Review logs -docker logs bird1 | grep -i error -``` - -### etcd Cluster Issues -```bash -# Check members -docker exec etcd1 etcdctl member list - -# Check status -docker exec etcd1 etcdctl endpoint status --write-out=table - -# Check health -docker exec etcd1 etcdctl endpoint health - -# If split-brain, backup and reset -docker exec etcd1 etcdctl snapshot save /tmp/backup.db -# Then make clean && make deploy-local -``` - -## Performance Expectations - -- **Deployment**: `make deploy-local` converges <2min (host with >8GB RAM) -- **etcd quorum reads**: <10ms for TINC peer propagation -- **BGP convergence**: <30s with BFD, ~90s without -- **TINC overhead**: <50ms additional latency vs direct -- **Go daemon discovery**: <10s for 5 nodes via mDNS -- **Docker images**: BIRD ~100MB, TINC ~80MB, monitoring ~200MB - -## Edge Cases & Considerations - -### Port Conflicts -- BIRD uses 179 (BGP) over TINC tun0 -- TINC uses 655/udp -- Resolution: Custom Docker networks (mesh-net, cluster-net) - -### macOS Quirks -If using Docker Desktop on macOS: -- Volume mounts slow: Use `--platform linux/amd64` in Dockerfiles -- /dev/net/tun not available: Use Docker Machine or Linux VM - -### Config Drift Prevention -- All configs versioned in git -- Ansible with `--diff` shows changes before apply -- Make target: `make validate` runs dry-run - -### Security Notes -- RSA-2048 keys generated via `tinc generate-keys` -- BGP passwords in .env (not committed) -- etcd encryption at rest (future: Sprint 4) -- TINC key rotation via Ansible cron (future: Sprint 3) - -## Sprint 1 Success Metrics (October 2024 - 3 nodes) - -Initial deployment with 3-node topology: - -- [x] `make deploy-local` functional in <2min -- [x] BGP sessions established (`birdc show protocols`) -- [x] TINC mesh up (layer 2 connectivity verified) -- [x] etcd propagation working (`etcdctl get /peers`) -- [x] Prometheus scraping metrics -- [x] Integration test passes - -## Sprint 1.5 Enhancements (Completed) - -**Objective**: Scale from 3 to 5 nodes with dynamic configuration - -**Achievements**: -- [x] Dynamic BGP peer configuration via `protocols.conf.j2` template -- [x] Full mesh topology auto-generates N-1 peers per node -- [x] Environment variables: `NODE_IP`, `NODE_ID`, `TOTAL_NODES` added to bird containers -- [x] Scalable test suite with dynamic node count detection -- [x] Per-node validation loops (tests all nodes, not just node1) -- [x] Full mesh ping validation (N×(N-1) pairs) -- [x] Integration tests pass with 5 nodes (20/20 pings successful) -- [x] BGP sessions: 4/4 per node (full mesh verified) - -## Architecture Philosophy - -**Design Principles**: -1. **Separation of concerns**: Root for metadata, docs for knowledge, docker for isolation, configs for templates, ansible for orchestration, daemon for custom logic -2. **Modularity**: Atomic Ansible roles, Go packages for testability -3. **Idempotency**: Ansible --diff prevents config drift -4. **Performance optimization**: Multi-stage Docker builds reduce image sizes ~20-30% -5. **Fail-fast validation**: Checkpoints after each implementation step - -**Trade-offs Accepted**: -- Greater directory nesting (e.g., `roles/bird/tasks/`) increases path lengths but improves discoverability -- Potential config drift if vars not versioned, mitigated with `make validate` -- Docker Desktop on macOS has slow volumes, workaround with platform flags - -## Next Steps - -After completing Step 6, proceed with: -1. **Sprint 2**: Go daemon Phase 2 (mDNS discovery), Ansible roles completion -2. **Sprint 3**: Key distribution automation, config sync (rsync + inotify) -3. **Sprint 4**: Route Reflectors, RPKI validation, security hardening - -## References - -- **Arquitectura**: Detailed technical design document in this repo -- **PLAN-OPTIMIZADO-GROK.md**: Optimization decisions (28 files vs 42-45) -- **PROMPT-BGP-NETWORK.md**: Full project prompt with architecture diagrams -- BIRD 2.x: https://bird.network.cz/ (using BIRD 2.0.12) -- TINC 1.0: https://www.tinc-vpn.org/ -- etcd: https://etcd.io/ diff --git a/Makefile b/Makefile index ce847ea..1b3d97c 100644 --- a/Makefile +++ b/Makefile @@ -1,52 +1,16 @@ -.PHONY: deploy-local test monitor clean validate help status tinc-bootstrap -.PHONY: test-fast test-env test-configs test-builds test-integration test-e2e test-all +.PHONY: help status clean -deploy-local: ## Deploy local environment - docker compose up -d --build - -test: ## Run integration tests - ./tests/integration/test_bgp_peering.sh - -monitor: ## Open monitoring dashboard - @echo "Opening Grafana at http://localhost:3000" - @echo "Opening Prometheus at http://localhost:9090" - @xdg-open http://localhost:3000 2>/dev/null || open http://localhost:3000 2>/dev/null || echo "Please open http://localhost:3000 manually" - -clean: ## Clean up - docker compose down -v - -validate: ## Validate configs - @if [ -d ansible ]; then ansible-playbook ansible/site.yml --syntax-check; else echo "Ansible not yet implemented"; fi - -# Test targets -test-fast: ## Run fast validation tests (parallel) - @echo "=== Running fast validation tests ===" - $(MAKE) -j4 test-env test-configs test-builds - -test-env: ## Test environment variables - @./tests/validation/test_env_vars.sh - -test-configs: ## Test configuration templates - @./tests/validation/test_configs.sh - -test-builds: ## Test Docker builds - @./tests/validation/test_docker_builds.sh - -test-integration: ## Run integration tests - @./tests/integration/test_bgp_peering.sh - -test-e2e: ## Run end-to-end tests - @./tests/e2e/test_full_stack.sh - -test-all: test-fast test-integration test-e2e ## Run all tests - -status: ## Show status of all containers - @docker ps --format "table {{.Names}}\t{{.Status}}" | grep -E "NAME|bird|tinc|etcd|prom" +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' -tinc-bootstrap: ## Bootstrap TINC mesh connectivity - ./tinc_bootstrap.sh +status: ## Show status of containers + @docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "NAME|bird|netmaker|netclient|mq" || echo "No containers running" -help: ## Show this help - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +clean: ## Stop and remove all project containers + @for dir in deploy/*/; do \ + echo "Cleaning $$dir..."; \ + (cd "$$dir" && docker compose down -v 2>/dev/null) || true; \ + done .DEFAULT_GOAL := help + diff --git a/PLAN-OPTIMIZADO-GROK.md b/PLAN-OPTIMIZADO-GROK.md deleted file mode 100644 index 54c7f79..0000000 --- a/PLAN-OPTIMIZADO-GROK.md +++ /dev/null @@ -1,226 +0,0 @@ -# Plan Optimizado BGP - Análisis Grok - -**Fecha**: 2025-10-27 -**Objetivo**: Reducir de 42-45 archivos a ~28-32, priorizar funcional sobre documentación - ---- - -## Respuestas a Preguntas Específicas - -### 1. CONTRIBUTING.md y CHANGELOG.md -**Decisión**: **POSTERGAR hasta Sprint 2** -- CONTRIBUTING innecesario sin colaboración externa -- CHANGELOG irrelevante sin releases, usar `git log --oneline` -- Agregar cuando el equipo crezca o se publique repo - -### 2. LICENSE -**Decisión**: **POSTERGAR hasta Sprint 2** -- No afecta ejecución ni testing en dev local -- Permite revisar dependencies primero (BIRD BSD, TINC GPL2) -- Agregar al publicar en repo público -- Considerar MIT para compatibilidad Go daemon - -### 3. Docs (7 archivos) -**Decisión**: **Solo QUICKSTART.md + architecture/decisions.md** -- **Mantener**: - - `QUICKSTART.md`: Steps para `make deploy-local`, verificaciones - - `architecture/decisions.md`: ADRs (BIRD 3.x, TINC 1.0, etcd) -- **Postergar**: - - README duplicado en docs/ - - overview.md - - runbooks/ (deployment, troubleshooting) → para producción - - api/daemon-api.md → cuando daemon madure - -### 4. Docker entrypoints -**Decisión**: **MANTENER separados bird y tinc, UNIFICAR monitoring** -- Bird y TINC separados para: - - Restarts independientes (`docker restart bird1`) - - Debugging granular (logs por servicio) - - Healthchecks específicos -- Monitoring unificado (Prometheus + Grafana en un solo Dockerfile) -- Evitar supervisord (viola single-responsibility) - -### 5. Molecule testing -**Decisión**: **Solo integration tests básicos en Sprint 1** -- Molecule overhead en setup (molecule.yml, custom images) -- Priorizar `test_bgp_peering.sh` simple: - - Spin compose - - Assert BGP sessions established (`birdc show protocols`) - - Verificar propagación etcd -- Molecule para Sprint 2 cuando roles sean maduros - ---- - -## Plan Optimizado por Categorías - -### MANTENER (~20 archivos) ✅ - -**Root (5)**: -- `.gitignore` - Go builds, .env -- `README.md` - Overview + QUICKSTART mergeado -- `Makefile` - Targets: deploy-local, test, monitor, clean, validate, help -- `docker-compose.yml` - 3 bird + 3 tinc + 3 etcd + prometheus + grafana -- `.env.example` - ETCD_INITIAL_CLUSTER, BIRD_PASSWORD -- `.editorconfig` - Code style desde día 1 - -**Docs (2)**: -- `QUICKSTART.md` - git clone, make deploy-local, verificaciones -- `architecture/decisions.md` - ADRs técnicos - -**Docker (6)**: -- `bird/Dockerfile` + `bird/entrypoint.sh` -- `tinc/Dockerfile` + `tinc/entrypoint.sh` -- `monitoring/Dockerfile` + `monitoring/entrypoint.sh` (unificado) - -**Configs (8)**: -- `bird/bird.conf.j2` - Router ID, protocols BGP -- `bird/filters.conf` - Route-maps -- `bird/protocols.conf` - Peers over TINC IPs -- `tinc/tinc.conf.j2` - Mode=switch, Cipher=AES-256 -- `tinc/tinc-up.j2` - ip link set, etcd put /peers -- `tinc/tinc-down.j2` - Cleanup -- `etcd/init.sh` - Cluster bootstrap -- `prometheus/prometheus.yml` - Scrape configs - -**Ansible (5)**: -- `ansible.cfg` -- `site.yml` -- `inventory/hosts.ini` -- `group_vars/all.yml` -- `roles/bird/tasks/main.yml` -- `roles/tinc/tasks/main.yml` - -**Daemon Go (5)**: -- `go.mod` -- `cmd/bgp-daemon/main.go` -- `pkg/discovery/mdns.go` - mDNS over TINC -- `pkg/types/types.go` -- `README.md` - Build/run instructions - -**CI/CD (1)**: -- `.github/workflows/ci.yml` - Lint Go, run make test - -**Tests (1)**: -- `integration/test_bgp_peering.sh` - -**Total**: ~25-28 archivos - ---- - -### POSTERGAR (Sprint 2) 📅 - -**Root**: -- `LICENSE` - Cuando se publique repo -- `CONTRIBUTING.md` - Multi-dev collaboration -- `CHANGELOG.md` - Auto-gen en releases - -**Docs**: -- `docs/README.md` - Redundante -- `architecture/overview.md` - Expandir decisions.md después -- `runbooks/deployment.md` - Para producción -- `runbooks/troubleshooting.md` - Basado en issues reales -- `api/daemon-api.md` - OpenAPI cuando daemon sea estable - -**Tests**: -- `molecule/default/molecule.yml` - Testing avanzado Ansible - -**Docker** (si aplica): -- Separar monitoring si crece (alertmanager) - ---- - -### ELIMINAR ❌ - -- Cualquier README duplicado si QUICKSTART cubre todo -- Docs no mencionadas en "Mantener" o "Postergar" -- Configs innecesarias (si no se usan en Sprint 1) - ---- - -## Justificaciones Técnicas Clave - -### Por qué reducir docs: -- **Acelera iteraciones**: Cambios en `tinc-up.j2` no requieren updates masivos -- **Reduce cognitive load**: Foco en código funcional vs polish -- **Mitiga con**: Inline comments + godoc en Go - -### Por qué separar entrypoints Docker: -- **Modularidad**: Restart independiente crítico en dev -- **Debugging**: `docker logs bird1` específico -- **Healthchecks**: Por servicio (`birdc show status`) -- **SRP**: Single Responsibility Principle - -### Por qué postergar Molecule: -- **Overhead**: Setup toma tiempo (custom images, drivers) -- **MVP approach**: Scripts bash suficientes para validación rápida -- **Integración temprana**: Mejor iterar rápido en daemon Go - -### Por qué mantener .editorconfig: -- **Consistencia desde día 1**: Go/Ansible/YAML -- **Evita drifts**: Tabs vs spaces, line endings - ---- - -## Flujo de Trabajo Optimizado - -```bash -# Sprint 1 - Setup (~1-2 horas) -git clone -cd BGP -cp .env.example .env -make deploy-local # docker-compose up -d - -# Validación -make test # integration/test_bgp_peering.sh -make monitor # Abre Grafana localhost:3000 - -# Iteración -vim configs/bird/bird.conf.j2 -make validate # ansible-playbook --syntax-check -docker restart bird1 - -# Cleanup -make clean # docker-compose down -v -``` - ---- - -## Métricas de Éxito Sprint 1 - -- [ ] `make deploy-local` funciona en <5min -- [ ] BGP sessions established (birdc show protocols) -- [ ] TINC mesh up (tinc dump reachable) -- [ ] etcd propagation working (etcdctl get /peers) -- [ ] Prometheus scraping metrics -- [ ] Integration test pasa - ---- - -## Trade-offs Aceptados - -1. **Menos docs → Mayor reliance en code comments** - - Mitigation: Godoc + inline comments exhaustivos - -2. **Sin Molecule → Menos coverage en edge cases** - - Mitigation: Integration tests cubren happy path - -3. **Sin LICENSE → All rights reserved default** - - Mitigation: Placeholder UNLICENSED, agregar en Sprint 2 - -4. **Monitoring unificado → Un container más pesado** - - Mitigation: Multi-stage build, aceptable para dev local - ---- - -## Próximos Pasos - -1. **Crear estructura** con archivos "Mantener" -2. **Makefile funcional** con todos los targets -3. **docker-compose.yml** completo (9 services) -4. **Test smoke** del stack -5. **Commit inicial**: `feat: initial optimized structure (28 files)` - ---- - -**Recomendación Final de Grok**: -Este setup reduce tiempo de bootstrap de horas a 1-2 horas, con dependencies claras. Escalable a producción con adiciones mínimas. Git branches para features pospuestas. diff --git a/PROMPT-BGP-NETWORK.md b/PROMPT-BGP-NETWORK.md deleted file mode 100644 index 7f983d0..0000000 --- a/PROMPT-BGP-NETWORK.md +++ /dev/null @@ -1,565 +0,0 @@ -# Prompt para Claude/Grok: Proyecto BGP Network Architecture - -## 🎯 Objetivo del Proyecto - -Diseñar e implementar **BGP Network System** - un framework de red distribuida con lógica BGP que integra: -- **TINC v1.0**: Mesh VPN Layer 2 (switch mode) para conectividad overlay segura -- **BIRD 3.x**: Routing daemon con soporte BGPv4 + IPv6 (MP-BGP) -- **Ansible**: Orquestación para provisioning y config management continuo -- **Sistema Custom de Propagación**: Discovery automático, key distribution, config sync, health monitoring -- **CI/CD Automático**: Per-commit deployment con rolling updates - ---- - -## 🏗️ Arquitectura Confirmada (según análisis Grok) - -``` -┌─────────────────┐ Git Push ┌─────────────────┐ Rolling Deploy ┌─────────────────┐ -│ BGP Config │ ─────────────► │ GitLab CI/CD │ ──────────────────► │ BGP Edge Node │ -│ Repository │ │ + Ansible │ │ (TINC + BIRD) │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - ▼ ▼ ▼ -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ etcd Storage │ │ Prometheus │ │ TINC Mesh │ -│ - Configs │ │ - Telemetry │ │ - Layer 2 VPN │ -│ - Keys │ │ - Metrics │ │ - BGP Peering │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ -``` - ---- - -## 📋 Decisiones Técnicas Confirmadas - -### 1. BIRD Specifications ✅ - -**Decisión de Grok:** -- **Versión**: BIRD 3.x (3.1.4+) - current, soporte MP-BGP maduro -- **Protocolos**: BGPv4 + IPv6 (multiprotocol via RFC 4760) -- **Deployment**: - - **Debian/Servidores**: Docker containers con systemd orchestration - - **OpenWrt/Gateways**: Native via opkg (custom feeds para 3.x) -- **Features**: RPKI validation (RFC 6811), BFD integration, route reflectors - -**Justificación técnica:** -- BIRD 3.x reduce config boilerplate 30-40% vs 1.6 -- MP-BGP unificado elimina necesidad de daemons separados -- Overhead: ~100MB por 10k routes (optimizable con `channel bgp limit`) -- Performance: Maneja 1M routes con <5% CPU en hardware moderno -- Trade-off: Mayor consumo memoria vs 1.6, pero mejor reconvergencia (<30s con BFD) - -**Alternativas descartadas:** -- BIRD 1.6: EOL, syntax obsoleto -- FRR: 2x overhead en memoria en hardware embebido -- Quagga: Obsoleto (forked a FRR) - ---- - -### 2. Ansible Alcance ✅ - -**Decisión de Grok:** -- **Provisioning inicial**: Instalación de BIRD/TINC via roles customizados -- **Config management continuo**: `ansible-pull` cada 5 min desde repo Git -- **Integración CI/CD**: GitHub Actions/GitLab CI con pipelines automáticos - -**Workflow:** -```bash -# Provisioning inicial -ansible-playbook -i inventory site.yml --tags provision - -# Config management continuo (ejecutado por cron en cada nodo) -ansible-pull -U git@repo:playbooks.git -i localhost local.yml - -# CI/CD pipeline -git commit → webhook → ansible lint → dry-run → manual approval → deploy -``` - -**Justificación técnica:** -- Agentless (SSH-based): Ideal para OpenWrt con dropbear -- Idempotencia: Evita config drifts en producción -- Ansible-pull: Mitiga issues de push en nodos detrás de firewalls -- Performance: <1min por nodo en deploys -- Trade-off: No realtime, pero suficiente para config changes - -**Roles principales:** -- `role/bird`: Instalación, configs, filters BGP -- `role/tinc`: Setup mesh, key management, tinc-up scripts -- `role/monitoring`: Prometheus exporters, Grafana dashboards - ---- - -### 3. Stack Kafka + IPFS: **NO NECESARIOS** ✅ - -**Decisión de Grok: Usar alternativas más simples** - -#### Reemplazos propuestos: - -**Para Telemetría (reemplaza Kafka):** -- **Prometheus**: Push metrics via HTTP, blackbox exporters -- **Ventajas**: 50-70% menor latency vs Kafka, sin Zookeeper dependency -- **Overhead**: 50MB/nodo vs 200MB con Kafka -- **Métricas**: BGP flaps, TINC peer status, route counts - -**Para Config Storage (reemplaza IPFS):** -- **etcd**: Key-value store con watch API, HA con raft -- **Ventajas**: Realtime sync, integración con Ansible (etcd3 module) -- **Storage**: <100MB/nodo vs IPFS overhead de 10% bandwidth -- **Data**: bird.conf, tinc.conf, RSA keys (encrypted) - -**Justificación técnica:** -- Sistema custom de propagación ya provee low-latency sync -- Kafka overkill para escala objetivo (50 nodos iniciales) -- IPFS slow en cold starts, problemas en OpenWrt con low storage -- Trade-off: etcd centralizado pero HA, vs IPFS fully distributed - -**Alternativas descartadas:** -- Consul: Más pesado que etcd -- Git para configs: No realtime -- MQTT: Considerado, pero etcd mejor integración con Ansible - ---- - -### 4. CI/CD Strategy ✅ - -**Decisión de Grok:** - -#### Triggers (ambos): -1. **Cambios en configs**: git diff en `bird.conf` / `tinc.conf` -2. **Nuevos peers**: hooks en directorio `tinc/hosts/` - -#### Alcance: -- **Rolling update de toda la red** (no single-node) -- **Batches**: `serial: 20%` en Ansible playbook -- **Zero-downtime**: <1min de outage por nodo durante roll - -#### Pipeline stages: -```yaml -stages: - - test # ansible-lint, molecule para roles - - validate # dry-run con --check, syntax bird.conf - - canary # deploy a 2 nodos test - - deploy # rolling update (manual approval para prod) - - rollback # automatic si flap masivo detectado -``` - -**Justificación técnica:** -- Per-commit asegura rapid feedback -- Rolling update: Consistencia en topología, evita blackholing -- Canary nodes: Early detection de errores -- Performance: <10min full rollout en red de 50 nodos -- Trade-off: Más lento que single-node, pero sin outages - -**Health checks:** -- BGP sessions: `birdc show protocols all | grep Established` -- TINC peers: `tinc dump reachable` -- Route propagation: Test de conectividad end-to-end - ---- - -### 5. Sistema Custom de Propagación ✅ - -**Decisión de Grok: Orden de prioridad** - -#### Priority 1: **Discovery Automático** 🥇 -- **Método**: mDNS over TINC para peer detection -- **Por qué primero**: Habilita auto-scaling sin registry central -- **Overhead**: <1% bandwidth -- **Implementación**: Daemon en Go parseando `tinc dump` - -#### Priority 2: **Key Distribution** 🥈 -- **Método**: Secure SCP con pre-shared keys -- **Por qué**: Mitiga manualidad de TINC 1.0 (no tiene invitaciones de 1.1) -- **Rotación**: Cron job mensual vía Ansible -- **Storage**: etcd con encryption at rest - -#### Priority 3: **Config Sync** 🥉 -- **Método**: rsync con inotify para realtime propagation -- **Por qué**: Asegura consistency de bird.conf en toda la red -- **Latency**: <5s para sync completo -- **Fallback**: Ansible-pull cada 5min - -#### Priority 4: **Health Monitoring** 🏅 -- **Método**: SNMP traps + Prometheus alerts -- **Por qué**: Último porque discovery/keys son blockers -- **Métricas**: Flap counts, peer uptime, route convergence time - -**Información propagada (del reporte Grok):** -```json -{ - "peers": { - "tinc_ips": ["10.0.0.1", "10.0.0.2"], - "rsa_keys": ["base64_key1", "base64_key2"], - "endpoints": ["203.0.113.1:655", "203.0.113.2:655"] - }, - "bgp_config": { - "as_numbers": [65001, 65002], - "prefixes": ["2001:db8:1::/48", "2001:db8:2::/48"], - "policies": ["export_to_peers", "import_from_transit"] - }, - "topology": { - "adjacencies": [["node1", "node2"], ["node2", "node3"]], - "graph": "networkx serialized format" - }, - "metrics": { - "rtt": "45ms", - "loss": "0.5%", - "flap_count": 3, - "uptime": "99.95%" - } -} -``` - -**Daemon custom specs:** -- **Lenguaje**: Go (cross-platform, bajo overhead) -- **Protocolo**: UDP multicast (239.255.0.1:9999) over TINC -- **Fanout limit**: 100 peers sin degradación -- **Edge cases**: Key compromise → rotate via cron - ---- - -## 🔧 Stack Tecnológico Final - -**Networking:** -- BGP: BIRD 3.x (MP-BGP, RPKI, BFD) -- VPN: TINC 1.0 (switch mode, RSA-2048, AES-256) -- Orchestration: Docker (Debian) + systemd (ambos) - -**Storage & Telemetry:** -- Config: etcd cluster (3 nodes HA, raft consensus) -- Metrics: Prometheus + Grafana + Alertmanager -- Logs: syslog-ng → centralized (no ELK, too heavy) - -**Automation:** -- CI/CD: GitLab CI / GitHub Actions -- Config Management: Ansible (push + pull hybrid) -- Testing: Molecule para roles, Mininet para network simulation - -**Monitoring:** -- Prometheus (metrics): bird_exporter, tinc_exporter -- Grafana (dashboards): BGP sessions, route counts, peer status -- BFD: Liveness detection (<30s reconvergencia) - ---- - -## 🎯 Tareas de Implementación - -### Sprint 1: Fundamentos (Semana 1-2) - ALTA PRIORIDAD - -#### 1.1 Setup inicial del repositorio BGP/ -- Estructura de directorios -- Docker compose para dev local (BIRD + TINC + etcd) -- Makefile para automatización (`make deploy-local`, `make test`) - -#### 1.2 BIRD 3.x deployment básico -- Container image con BIRD 3.1.4 -- bird.conf template con BGPv4 + IPv6 -- systemd unit file para orchestration -- Test: 2 nodos BGP peer via TINC - -#### 1.3 TINC 1.0 mesh básico (3 nodos) -- Configuración switch mode -- RSA key generation automático -- tinc-up scripts para BIRD integration -- Connectivity tests (ping over tunnel) - -#### 1.4 etcd cluster setup -- 3 nodes HA con raft -- Storage de configs (bird.conf, tinc.conf) -- Ansible integration (etcd3 module) - ---- - -### Sprint 2: Automation & Propagación (Semana 3-4) - MEDIA-ALTA - -#### 2.1 Sistema custom de propagación - Phase 1: Discovery -- Daemon Go para mDNS over TINC -- Parsing de `tinc dump` para peer detection -- Tests: Auto-discovery de 5 nodos en <10s - -#### 2.2 Ansible roles completos -- `role/bird`: Install, config, filters -- `role/tinc`: Mesh setup, key rotation -- `role/monitoring`: Prometheus exporters -- Playbook: `site.yml` con idempotencia - -#### 2.3 CI/CD pipeline básico -- GitHub Actions workflow -- Stages: lint → validate → dry-run -- Config validation (bird --parse-only) - -#### 2.4 Prometheus + Grafana -- bird_exporter deployment -- Dashboards: BGP sessions, route counts -- Alerting rules (flap >5/min) - ---- - -### Sprint 3: Production Hardening (Semana 5-6) - MEDIA - -#### 3.1 Sistema custom - Phase 2: Key Distribution -- Secure SCP con pre-shared keys -- Integration con etcd para key storage -- Rotación mensual via Ansible cron - -#### 3.2 CI/CD completo -- Deploy automático post-merge (con approval) -- Rolling update con canary nodes -- Rollback automático en failures -- Health checks: birdc status, tinc connectivity - -#### 3.3 Config Sync realtime -- rsync + inotify para bird.conf changes -- Fallback: ansible-pull cada 5min -- Validation: Config consistency checks - -#### 3.4 Testing automation -- Mininet para network topology simulation -- Chaos engineering (node failures, partition tolerance) -- Performance tests: iperf3, mtr - ---- - -### Sprint 4: Escalabilidad & Security (Semana 7+) - BAJA - -#### 4.1 Route Reflectors -- BIRD config para RR en servidores Debian -- Reduce sessions de O(n²) a O(n) -- Testing con 50+ nodos - -#### 4.2 Sistema custom - Phase 3: Health Monitoring -- SNMP traps para alerts -- Integration con Prometheus -- Dashboards: Uptime, flap rates, latency - -#### 4.3 Security hardening -- BGP MD5 authentication -- TINC key rotation automation -- etcd encryption at rest -- RPKI validation - -#### 4.4 High availability -- etcd multi-region replication -- BGP multi-path para redundancy -- Automated failover tests - ---- - -## 🤖 Protocolo de Colaboración Claude ↔ Grok - -### Workflow: - -**Claude (arquitecto/implementador)**: -1. Diseña estructura inicial de código -2. Implementa Ansible roles y configs -3. Crea tests y validaciones -4. Documenta decisiones técnicas - -**Grok (revisor/consultor)** - vía Playwright MCP: -1. Revisa arquitectura propuesta -2. Sugiere optimizaciones técnicas -3. Identifica edge cases y trade-offs -4. Propone mejoras de performance/security - -### Formato de Consulta: - -```bash -# Claude exporta contexto -mkdir -p ~/repos/BGP/.context-for-grok/ -cp README.md ~/repos/BGP/.context-for-grok/ -cp architecture.md ~/repos/BGP/.context-for-grok/ -cp ansible/roles/bird/tasks/main.yml ~/repos/BGP/.context-for-grok/ - -# Claude usa Playwright MCP para Grok -# (desde interfaz de Claude Code) -# → Upload context files -# → Ask: "Review this BIRD config for production edge cases" -# → Extract response y apply feedback -``` - ---- - -## 📊 Métricas de Éxito - -### Funcionales: -- [ ] 3+ nodos BGP en TINC mesh functioning -- [ ] Route announcements propagated en <30s -- [ ] CI deploy completo en <10min -- [ ] Configs persistidas en etcd con <5s sync - -### Performance: -- [ ] Latency overlay: <50ms adicional vs direct -- [ ] BGP convergence: <30s con BFD -- [ ] Prometheus scrape: >10 metrics/sec sustained -- [ ] etcd latency: <2s para read/write - -### Operacionales: -- [ ] Zero-downtime deploys (rolling update) -- [ ] Automated rollback funcional (<2min) -- [ ] Monitoring lag: <1min -- [ ] Documentation: 100% cobertura - ---- - -## 🔐 Consideraciones de Seguridad - -**BGP:** -- MD5 authentication en sessions (RFC 5925) -- Prefix filtering (max-prefix limits) -- RPKI validation contra hijacking -- Route-map policies estrictas - -**TINC:** -- RSA-2048 keys (upgrade path a 4096) -- AES-256 cipher (validar vs ChaCha20 en ARM) -- Key rotation mensual automatizada -- Firewall: Solo UDP 655 desde IPs conocidas - -**etcd:** -- TLS para client-server communication -- Encryption at rest para secrets -- RBAC para access control -- Regular backups con etcdctl snapshot - -**CI/CD:** -- Signed commits (GPG) -- Ansible Vault para secrets -- Manual approval para production deploys -- Audit logs completos - ---- - -## 🚀 Entregables Esperados - -### 1. Repositorio BGP/ funcional: -``` -BGP/ -├── docker-compose.yml # Stack completo (BIRD, TINC, etcd, Prometheus) -├── Makefile # Comandos: deploy-local, test, monitor -├── ansible/ -│ ├── roles/ -│ │ ├── bird/ # BIRD 3.x deployment -│ │ ├── tinc/ # TINC 1.0 mesh -│ │ └── monitoring/ # Prometheus exporters -│ ├── inventory/ -│ │ ├── hosts # Static inventory -│ │ └── dynamic_tinc.py # Dynamic discovery via TINC -│ ├── site.yml # Main playbook -│ └── group_vars/all.yml # Configs globales -├── daemon/ # Sistema custom propagación (Go) -│ ├── main.go -│ ├── discovery.go # mDNS -│ ├── keydist.go # Key distribution -│ └── sync.go # Config sync -├── configs/ -│ ├── bird/ # Templates bird.conf -│ └── tinc/ # Templates tinc.conf -├── .github/workflows/ -│ └── deploy.yml # CI/CD pipeline -├── docs/ -│ ├── architecture.md # Diagramas UML -│ ├── runbooks/ # Ops procedures -│ └── api.md # API del daemon custom -└── tests/ - ├── mininet/ # Network simulation - └── molecule/ # Ansible role tests -``` - -### 2. Demo funcional: -```bash -cd ~/repos/BGP - -# Levantar stack local (3 nodos) -make deploy-local - -# Validar conectividad -make test # BGP sessions, TINC peers, etcd health - -# Monitoreo -make monitor # Abre Grafana dashboards (localhost:3000) - -# Deploy real -ansible-playbook -i inventory/hosts site.yml --check # Dry-run -ansible-playbook -i inventory/hosts site.yml # Deploy -``` - -### 3. CI/CD pipeline ejecuta: -1. **Lint**: ansible-lint, yamllint -2. **Validate**: bird --parse-only, tinc --config-test -3. **Test**: Molecule en Docker, unit tests del daemon Go -4. **Canary Deploy**: 2 nodos test -5. **Production Deploy**: Rolling update (manual approval) -6. **Health Check**: birdc status, tinc dump, etcd health -7. **Rollback**: Automático si >10% nodos fallan - ---- - -## ❓ Preguntas Resueltas (confirmadas por Grok) - -1. **BIRD Implementation**: ✅ BIRD 3.x (current, MP-BGP), containers + systemd -2. **TINC version**: ✅ TINC 1.0 (stable, compatible con OpenWrt legacy) -3. **State management**: ✅ etcd para configs, Prometheus para metrics, custom daemon para propagación -4. **Deployment strategy**: ✅ Rolling updates con batches 20%, canary nodes, zero-downtime -5. **Testing strategy**: ✅ Molecule (Ansible), Mininet (network sim), Chaos engineering - ---- - -## 🎯 Contexto de Uso - -**User profile**: Desarrollador experimentado con networking en comunidades LibreMesh - -**Project philosophy** (del CLAUDE.md): -- **Excellence over speed**: Soluciones correctas, no patches temporales -- **Finish what you start**: No TODOs sin resolver, no planes incompletos -- **Automation-first**: Si se repite 3x, construir herramienta -- **Integration priority**: Soluciones holísticas, no workarounds -- **Low-profile contributions**: El trabajo habla, minimizar autopromoción - -**Communication style**: -- Técnico, basado en hechos, sin grandilocuencia -- Commits: `(): brief description` + "AI-assisted development" -- Documentación: Solo cuando lógica no es obvia o hay context crítico - ---- - -## 📚 Referencias Técnicas - -**RFCs:** -- RFC 4271: BGP-4 protocol -- RFC 4760: Multiprotocol Extensions for BGP-4 -- RFC 5925: TCP-AO (BGP MD5 successor) -- RFC 6811: RPKI validation - -**BIRD Docs:** -- https://bird.network.cz/ (BIRD 3.x manual) -- Migration guide 1.6 → 2.x → 3.x - -**TINC Docs:** -- https://www.tinc-vpn.org/documentation/ (TINC 1.0 reference) -- Switch mode vs router mode comparison - -**Ansible Best Practices:** -- https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html -- Ansible Vault, dynamic inventory - -**Papers:** -- "Comparison of Routing Protocols for Wireless Mesh Networks" (IEEE 2018) -- "Delay-Based Metric Extension for Babel" (2011) - ---- - -## 🚀 Próximos Pasos - -1. **Validar con Pablo**: Confirmar arquitectura y prioridades -2. **Crear estructura inicial**: `mkdir -p` directorios, Makefile básico -3. **Sprint 1 execution**: BIRD + TINC local deployment -4. **Iterar con Grok**: Consultar edge cases durante implementación -5. **Documentar decisiones**: Mantener context transfer documents - ---- - -**Let's build a production-grade BGP network system con automation exhaustiva y resiliencia probada.** - ---- - -*Prompt versión 2.0 - Validado con Grok - 2025-10-27* -*Basado en análisis técnico profundo: BIRD 3.x, TINC 1.0, Ansible, etcd, Prometheus* diff --git a/README.md b/README.md index 0d1b1b2..0ca37d7 100644 --- a/README.md +++ b/README.md @@ -1,166 +1,200 @@ -# BGP Overlay Network over TINC Mesh +# BGP4mesh -A production-grade BGP routing framework with automated orchestration, combining BIRD 3.x, TINC 1.0 mesh VPN, etcd distributed storage, and custom Go daemon for peer discovery. +BGP route distribution over a Netmaker WireGuard mesh network. -## Stack +## Overview -- **BIRD 3.x**: BGP routing daemon (MP-BGP, RPKI validation) -- **TINC 1.0**: Layer 2 mesh VPN (switch mode, RSA-2048, AES-256) -- **etcd 3.5+**: Distributed config/state storage with HA -- **Ansible**: Infrastructure orchestration -- **Go daemon**: Custom propagation (mDNS discovery, key distribution, config sync) -- **Prometheus + Grafana**: Metrics and monitoring -- **Docker**: Service containerization +This project implements BGP peering between two autonomous systems, with routes distributed to mesh nodes via Netmaker (WireGuard-based VPN). -## Quick Start +``` +┌────────────────┐ ┌─────────────────────────────────┐ ┌────────────────┐ +│ rpi-isp │ │ laptop-border │ │ laptop-mesh │ +│ AS 65001 │◄──BGP──►│ AS 65000 │◄──WG───►│ mesh node │ +│ 172.30.0.1 │ :179 │ 172.30.0.100 │ :51821 │ │ +│ │ │ │ │ │ +│ BIRD │ │ BIRD + Netmaker + Caddy │ │ Netclient │ +│ announces: │ │ 44.30.127.1 (mesh) │ │ 44.30.127.x │ +│ 192.0.2.0/24 │ │ │ │ │ +│ 198.51.100/24 │ │ exports to BGP: │ │ receives: │ +│ 203.0.113/24 │ │ 44.30.127.0/24 │ │ ISP routes │ +└────────────────┘ └─────────────────────────────────┘ └────────────────┘ +``` -```bash -# Setup -cp .env.example .env -make deploy-local +## Components -# Verify (wait ~90s for convergence) -docker exec bird1 birdc show protocols -docker exec tinc1 tinc -n bgpmesh info -docker exec etcd1 etcdctl endpoint health +| Directory | Device | Function | Software | +|-----------|--------|----------|----------| +| `deploy/rpi-isp` | Raspberry Pi | Mock ISP, AS 65001 | BIRD 2 | +| `deploy/laptop-border` | Laptop | Border router AS 65000 + Netmaker server | BIRD 2, Netmaker, Caddy, Mosquitto | +| `deploy/laptop-mesh` | Laptop/other | Mesh node | Netclient | -# Monitor -make monitor # Opens Grafana at http://localhost:3000 +## Network addressing -# Test -make test-all +| Network | CIDR | Purpose | +|---------|------|---------| +| Physical LAN | 172.30.0.0/24 | BGP peering between rpi-isp and laptop-border | +| Netmaker mesh | 44.30.127.0/24 | WireGuard overlay, distributed to all mesh nodes | +| TEST-NET-1 | 192.0.2.0/24 | Announced by rpi-isp (RFC 5737) | +| TEST-NET-2 | 198.51.100.0/24 | Announced by rpi-isp (RFC 5737) | +| TEST-NET-3 | 203.0.113.0/24 | Announced by rpi-isp (RFC 5737) | -# Cleanup -make clean -``` +## Requirements + +- Docker Engine (not Docker Desktop - `network_mode: host` requires native Docker) +- Devices on same LAN for BGP peering (rpi-isp ↔ laptop-border) +- UDP connectivity for WireGuard (port 51821) -See [QUICKSTART.md](docs/QUICKSTART.md) for detailed instructions. +## Deployment order -## Common Commands +### 1. rpi-isp (Mock ISP) ```bash -# Container status -make status -docker ps - -# BIRD (BGP routing) -docker exec bird1 birdc show protocols # All protocols -docker exec bird1 birdc show protocols all peer1 # Peer detail -docker exec bird1 birdc show route # Routing table - -# TINC (VPN mesh) -docker exec tinc1 ip addr show tinc0 # Interface status - -# etcd (distributed storage) -docker exec etcd1 etcdctl member list # Cluster members -docker exec etcd1 etcdctl endpoint health # Cluster health -docker exec etcd1 etcdctl put /key "value" # Write -docker exec etcd1 etcdctl get /key # Read - -# Logs -docker logs -f bird1 # Follow logs -docker compose logs bird1 bird2 bird3 # Multiple services - -# Access containers -docker exec -it bird1 /bin/bash # Interactive shell +cd deploy/rpi-isp +# Edit bird.conf: set correct IPs +docker compose up -d ``` -## Project Structure +### 2. laptop-border (Border Router + Netmaker Server) +```bash +cd deploy/laptop-border + +# Create .env +cat < .env +SERVER_HOST=172.30.0.100 +MASTER_KEY=$(openssl rand -base64 32) +EOF + +# Generate TLS certificate (required for netclient) +mkdir -p certs +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout certs/server.key -out certs/server.crt \ + -subj "/CN=172.30.0.100" -addext "subjectAltName=IP:172.30.0.100" + +# Install CA on host +sudo cp certs/server.crt /usr/local/share/ca-certificates/netmaker.crt +sudo update-ca-certificates + +# Start services +docker compose up -d + +# Wait for netmaker to start, then create network +source .env +sleep 10 + +curl -sk -X POST "https://localhost/api/networks" \ + -H "Authorization: Bearer $MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{"netid": "mesh", "addressrange": "44.30.127.0/24"}' + +# Create enrollment key +curl -sk -X POST "https://localhost/api/v1/enrollment-keys" \ + -H "Authorization: Bearer $MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{"networks": ["mesh"], "tags": ["node"], "unlimited": true}' + +# Copy the "token" field from response, add to .env +echo "ENROLLMENT_TOKEN=" >> .env + +# Restart netclient with token +docker compose up -d --force-recreate netclient + +# Restart BIRD to detect netmaker interface +docker restart bird-border ``` -BGP/ -├── docker-compose.yml # 15 services (5 bird + 5 tinc + 5 etcd + monitoring) -├── Makefile # Build/deploy automation -├── configs/ # BIRD/TINC templates (Jinja2) -├── docker/ # Container builds -├── ansible/ # Infrastructure orchestration (4 roles) -├── daemon-go/ # Custom Go propagation daemon -├── tests/ # Validation, integration, E2E tests -└── docs/ # Documentation - -## Architecture - -- **Layer 2**: TINC mesh (switch mode) with UDP hole punching -- **Layer 3**: BIRD BGP sessions over TINC tunnels -- **State**: etcd cluster for peer propagation -- **Discovery**: Go daemon with mDNS over TINC interface -- **Monitoring**: Prometheus scraping BIRD metrics, Grafana dashboards -See [docs/architecture/decisions.md](docs/architecture/decisions.md) for design decisions. - -## Development +### 3. laptop-mesh (Mesh Node) ```bash -# Run all tests -make test-all - -# Test individual components -make test-env # Environment variables -make test-configs # Configuration templates -make test-builds # Docker builds -make test-integration # BGP/TINC/etcd integration -make test-e2e # Full stack workflow - -# Development workflow -vim configs/bird/bird.conf.j2 -make validate -docker restart bird1 bird2 bird3 -``` +cd deploy/laptop-mesh -## Requirements - -- Docker 24+ with Compose v2 -- Go 1.21+ (for daemon development) -- Ansible 2.16+ (for production deployment) -- >8GB RAM (>16GB recommended for parallel builds) +# Install CA certificate from border router +scp user@172.30.0.100:/path/to/certs/server.crt /tmp/netmaker.crt +sudo cp /tmp/netmaker.crt /usr/local/share/ca-certificates/netmaker.crt +sudo update-ca-certificates -## Performance Targets +# Enable IP forwarding +sudo sysctl -w net.ipv4.ip_forward=1 -- Deployment: <2min convergence -- BGP: <30s reconvergence with BFD -- etcd: <10ms quorum reads -- TINC: <50ms overhead vs direct +# Create .env with enrollment token from step 2 +echo "ENROLLMENT_TOKEN=" > .env -## Sprint Status +docker compose up -d +``` -### Sprint 2 Phase 1 (Completed 2025-10-28) +## Verification -- **Testing**: Makefile (20+ targets), CI coverage enforcement - - pkg/tinc: 92.7% coverage (11 test functions) - - pkg/discovery: 89.8% coverage (9 test functions) - - pkg/types: 100% coverage -- **Scaling**: 5-node Docker deployment (15 containers, etcd quorum) -- **Automation**: 4 Ansible roles (etcd, tinc, bird, bgp-daemon) -- **Docs**: Testing guide, Deployment guide, Status report +### BGP status (rpi-isp) +```bash +docker exec bird-isp birdc show protocols +docker exec bird-isp birdc show route +``` -**Commands**: +### BGP status (laptop-border) ```bash -cd daemon-go && make test-coverage # Run tests with coverage -cd daemon-go && make test-unit # Fast tests (skip integration) -make deploy-local # Docker 5-node -cd ansible && ansible-playbook -i inventory/hosts.ini playbook.yml # Ansible +docker exec bird-border birdc show protocols +docker exec bird-border birdc show route +docker exec bird-border birdc "show route export isp" ``` -**Next**: Sprint 2 Phase 2 (custom Grafana dashboards, additional integration tests) +### Netmaker status +```bash +# Server health +curl -sk https://172.30.0.100/api/server/health -### Sprint 1 (Completed) +# WireGuard interface +docker exec netclient wg show -Local 3-node MVP, Docker orchestration, basic tests, Grafana monitoring +# Mesh connectivity +ping -I 44.30.127.1 172.30.0.1 +``` + +## Ports + +| Port | Protocol | Service | Node | +|------|----------|---------|------| +| 179 | TCP | BGP | rpi-isp, laptop-border | +| 443 | TCP | Netmaker API (Caddy TLS) | laptop-border | +| 1883 | TCP | MQTT (Mosquitto) | laptop-border | +| 51821 | UDP | WireGuard | laptop-border | -### Roadmap +## Files -- **Sprint 2 Phase 2**: Unit tests completion, custom Grafana dashboards -- **Sprint 3**: Production hardening (rolling updates, chaos testing) -- **Sprint 4**: Scalability (route reflectors, RPKI, multi-region) +``` +deploy/ +├── laptop-border/ +│ ├── docker-compose.yml # BIRD, Netmaker, Caddy, Mosquitto, Netclient +│ ├── bird.conf # BGP config AS 65000 +│ ├── Caddyfile # TLS reverse proxy +│ ├── mosquitto.conf # MQTT broker +│ ├── Dockerfile # BIRD container +│ ├── entrypoint.sh +│ ├── certs/ # TLS certificates (generated) +│ └── SETUP.md +├── laptop-mesh/ +│ ├── docker-compose.yml # Netclient only +│ └── SETUP.md +└── rpi-isp/ + ├── docker-compose.yml # BIRD only + ├── bird.conf # BGP config AS 65001 + ├── Dockerfile + ├── entrypoint.sh + └── SETUP.md +``` -## License +## Known issues -TBD +- Netmaker v0.24.x requires HTTPS. Caddy provides TLS termination with self-signed certificates. +- `network_mode: host` does not work with Docker Desktop (uses VM). Use native Docker Engine. +- BIRD must be restarted after netclient creates the WireGuard interface to learn the route. +- `sysctls` in docker-compose is ignored with `network_mode: host`. Set `ip_forward` on the host. -## Contributing +## Security considerations -See [CONTRIBUTING.md](CONTRIBUTING.md) (coming in Sprint 2) +This setup uses insecure defaults for testing: ---- +- Self-signed TLS certificates +- MQTT broker allows anonymous connections +- MASTER_KEY stored in plaintext .env files -**AI-assisted development** +For production: use proper CA certificates, enable MQTT authentication, use secrets management. diff --git a/STATUS-SPRINT1.md b/STATUS-SPRINT1.md deleted file mode 100644 index 2a718e0..0000000 --- a/STATUS-SPRINT1.md +++ /dev/null @@ -1,718 +0,0 @@ -# BGP Network - Status Report Sprint 1 - -> ⚠️ **DOCUMENTO HISTÓRICO** - Este documento describe el estado de Sprint 1 (3 nodos) del 28 de octubre de 2024. -> -> **TODOS LOS ISSUES CRÍTICOS HAN SIDO RESUELTOS EN SPRINT 1.5** (Noviembre 2024): -> - ✅ TINC mesh connectivity: Resuelto con Subnet declarations en host files (fix de layer 2 ARP) -> - ✅ BGP sessions: Resueltas con dynamic peer configuration via protocols.conf.j2 -> - ✅ Escalado a 5 nodos en full mesh (21 containers) -> - ✅ 20/20 pings successful, 20/20 BGP sessions established -> -> Para el estado actual ver [docs/MANUAL_TESTING.md](docs/MANUAL_TESTING.md) - -**Fecha**: 2025-10-28 -**Branch**: master -**Último Commit**: 555f057 (docs: add essential commands to README and Makefile) -**Estado Git**: Clean working tree -**Fase**: Sprint 1 MVP (3 nodos) - **FUNCIONAL CON ISSUES CRÍTICOS** → **RESUELTO EN SPRINT 1.5** - ---- - -## 🎯 Resumen Ejecutivo - -### Estado General -- **Containers**: 9/9 running healthy -- **Servicios Operacionales**: 7/9 - - ✅ etcd cluster (3 nodos, <10ms latency) - - ✅ TINC interfaces (tinc0 configuradas correctamente) - - ✅ Prometheus + Grafana monitoring - - ⚠️ **TINC mesh connectivity incompleto** - - ❌ **BGP sessions bloqueadas** - -### Issues Críticos Bloqueantes -1. **TINC mesh sin conectividad entre nodos** → Bloquea BGP -2. **BGP sessions en estado "Active - Socket closed"** → No hay route propagation - -### Commits Recientes -``` -555f057 - docs: add essential commands to README and Makefile -7433ce7 - fix: resolve Docker deployment issues and BIRD 2.x compatibility -4c21cff - feat: implement BGP overlay network MVP (Sprint 1) -e2df43f - docs: initial project documentation -``` - ---- - -## ✅ Qué Funciona (Operacional) - -### 1. etcd Cluster - 100% Operacional -**Status**: ✅ Completamente funcional - -```bash -$ docker exec etcd1 etcdctl endpoint health -127.0.0.1:2379 is healthy: successfully committed proposal: took = 3.65ms -``` - -**Características**: -- 3 nodos con Raft consensus -- Quorum establecido -- Latency: 3.65ms (target <10ms) ✓ -- Read/Write operations funcionando -- Volumes persistentes configurados - -**Evidencia**: -- Health checks passing -- Member list completo (etcd1, etcd2, etcd3) -- Prometheus scraping metrics - -### 2. TINC Interfaces - 90% Funcional -**Status**: ⚠️ Interfaces up, mesh incompleto - -```bash -$ docker exec tinc1 ip addr show tinc0 -3: tinc0: mtu 1400 - inet 10.0.0.1/24 scope global tinc0 - inet6 2001:db8::1/64 scope global -``` - -**Funcionando**: -- ✅ Containers con CAP_NET_ADMIN -- ✅ RSA-2048 key generation -- ✅ tinc0 interface creada -- ✅ IP assignment correcto (10.0.0.{1,2,3}/24) -- ✅ IPv6 assignment (2001:db8::{1,2,3}/64) -- ✅ MTU 1400 configurado -- ✅ entrypoint.sh rendering templates - -**NO Funcionando**: -- ❌ Peer-to-peer connections no establecidas -- ❌ Host keys no distribuidas entre nodos -- ❌ Falta ConnectTo directives en tinc.conf - -### 3. Docker Orchestration - 100% Funcional -**Status**: ✅ Todos los containers healthy - -``` -CONTAINER STATUS HEALTH ---------- ------ ------ -bird1 Up 30min healthy -bird2 Up 30min healthy -bird3 Up 30min healthy -tinc1 Up 30min healthy -tinc2 Up 30min healthy -tinc3 Up 30min healthy -etcd1 Up 30min n/a -etcd2 Up 30min n/a -etcd3 Up 30min n/a -prometheus Up 30min healthy -``` - -**Características**: -- docker-compose.yml con 9 servicios -- 2 networks: mesh-net (bridge), cluster-net (internal) -- 3 volumes persistentes (etcd data) -- Health checks configurados -- Port mapping correcto - -### 4. Monitoring Stack - 100% Operacional -**Status**: ✅ Prometheus + Grafana running - -**Acceso**: -- Prometheus: http://localhost:9090 ✓ -- Grafana: http://localhost:3000 ✓ (admin/admin) - -**Funcionalidad**: -- Prometheus scraping targets -- Multi-stage Docker build funcionando -- Supervisor managing both processes - -**Pendiente**: -- Custom Grafana dashboards (Sprint 2) -- BIRD exporter integration (Sprint 2) - -### 5. Configuration Rendering - 100% Funcional -**Status**: ✅ Templates generando configs correctamente - -**Templates Working**: -- `bird.conf.j2` → `/var/run/bird/bird.conf` ✓ -- `tinc.conf.j2` → `/var/run/tinc/bgpmesh/tinc.conf` ✓ -- `tinc-up.j2` → executable script ✓ -- `tinc-down.j2` → executable script ✓ - -**Rendering Method**: Python inline (jinja2 library) - ---- - -## ❌ Qué NO Funciona (Critical Path) - -### 1. BGP Sessions - BLOQUEADAS -**Status**: ❌ Todas las sessions en "Active - Socket closed" - -```bash -$ docker exec bird1 birdc show protocols -Name Proto Table State Since Info -peer1 BGP --- start 04:42:14.521 Active Socket: Connection closed -peer2 BGP --- start 04:42:14.521 Active Socket: Connection closed -``` - -**Root Cause**: -- BIRD intenta conectar a 10.0.0.2 y 10.0.0.3 (peers via TINC) -- TINC mesh no tiene conectividad entre nodos -- TCP socket al puerto 179 falla - -**Impacto**: -- ❌ No hay BGP route propagation -- ❌ No se puede testear route exchange -- ❌ Integration tests failing (bgp peering check) - -**Configuración BIRD** (protocols.conf): -``` -protocol bgp peer1 { - description "BGP peer at 10.0.0.2"; - local 10.0.0.1 as 65000; - neighbor 10.0.0.2 as 65000; # ← UNREACHABLE - ipv4 { import all; export all; }; -} -``` - -### 2. TINC Mesh Connectivity - INCOMPLETA -**Status**: ❌ Sin conectividad entre nodos - -**Problema**: -- Cada nodo tiene su propio RSA keypair -- Cada nodo tiene su archivo `hosts/nodeX` -- **Pero**: Ningún nodo conoce las public keys de los otros -- **Pero**: No hay `ConnectTo` directives configuradas - -**Evidencia del Issue**: -```bash -# Node1 tiene su key -$ docker exec tinc1 ls /var/run/tinc/bgpmesh/ -hosts/ rsa_key.priv rsa_key.pub tinc.conf tinc-down tinc-up - -$ docker exec tinc1 ls /var/run/tinc/bgpmesh/hosts/ -node1 # ← Solo tiene su propio host file - -# Debería tener: -# hosts/node1 hosts/node2 hosts/node3 -``` - -**Lo que Falta**: -1. Distribución de public keys entre nodos -2. Creación de host files para peers (`hosts/node2`, `hosts/node3`) -3. Agregar `ConnectTo` directives en tinc.conf: - ``` - ConnectTo = node2 - ConnectTo = node3 - ``` - -**Impacto**: -- ❌ No hay Layer 2 connectivity -- ❌ No se puede hacer ping entre nodos via TINC -- ❌ BGP sessions bloqueadas (dependen de TINC) - ---- - -## 🔍 Análisis Detallado por Componente - -### BIRD (BGP Routing) - 75% Implementado -**Container**: ✅ Running healthy -**Config**: ✅ Válido y renderizado -**Daemon**: ✅ Proceso corriendo -**BGP Sessions**: ❌ Bloqueadas - -**Implementado**: -- Dockerfile con bird2 package (BIRD 2.0.12) -- Template Jinja2 rendering via Python -- Protocol stack: device ✓, kernel ✓, static ✓ -- Peer definitions en protocols.conf -- Filter policies (accept-all para Sprint 1) -- Health checks configurados - -**Configuración Actual**: -```yaml -Router IDs: 192.0.2.{1,2,3} -BGP AS: 65000 (iBGP) -Peers: - - bird1 → bird2 (10.0.0.2) via TINC - - bird1 → bird3 (10.0.0.3) via TINC -Mode: iBGP (same AS) -Filters: Import all, export all (simplified Sprint 1) -``` - -**Issues**: -- Documentación menciona BIRD 3.x pero usa 2.x (funcional, solo nota) -- BGP MD5 auth keys definidos pero no configurados (Sprint 2) -- BFD no implementado (Sprint 3) - -**Siguiente Paso**: Una vez TINC mesh funcione, BGP debería establecer sessions automáticamente. - -### TINC (VPN Mesh) - 70% Implementado -**Container**: ✅ Running healthy -**Interface**: ✅ Up y configurado -**Mesh**: ❌ Sin conectividad - -**Implementado**: -- Dockerfile con tinc + etcd-client -- RSA key generation (2048-bit) -- tinc.conf rendering (Mode=switch, AES-256-CBC) -- tinc-up script (IP assignment + etcd propagation) -- tinc-down script (cleanup) -- NET_ADMIN capability - -**Configuración Actual**: -```yaml -Mode: switch (Layer 2) -Cipher: aes-256-cbc -Digest: sha256 -Port: 655 UDP -Interface: tinc0 -IPs: 10.0.0.{1,2,3}/24, 2001:db8::{1,2,3}/64 -``` - -**Missing**: -- Host file distribution mechanism -- ConnectTo directives -- Automated key exchange - -**Opciones para Resolver**: - -**Opción A: Script Manual** (rápido, MVP) -```bash -# Script que: -1. Extrae public keys de cada container -2. Crea host files para cada peer -3. Copia a /var/run/tinc/bgpmesh/hosts/ en cada nodo -4. Agrega ConnectTo a tinc.conf -5. Reinicia tincd -``` -Tiempo: 1-2h implementación, testing inmediato - -**Opción B: Go Daemon** (automatizado, Sprint 2) -```go -// Implementar en daemon-go: -- mDNS service discovery -- etcd watch on /peers/ -- Automatic key distribution -- Dynamic ConnectTo generation -``` -Tiempo: 4-6h implementación, mejor para producción - -### Go Daemon - 40% Implementado -**Status**: ✅ Compila y corre, ❌ Discovery incompleto - -**Implementado**: -```go -✅ go.mod con dependencies correctas -✅ etcd client connection -✅ etcd watch on /peers/ prefix -✅ Signal handling (SIGINT, SIGTERM) -✅ Graceful shutdown -✅ Logging infrastructure -✅ mDNS query structure (skeleton) -``` - -**TODOs Marcados en Código**: -```go -// pkg/types/types.go:31 -TODO Sprint 2: Add more peer metadata - -// pkg/discovery/mdns.go:75 -TODO Sprint 2: Implement AdvertiseService - -// cmd/bgp-daemon/main.go:105 -TODO Sprint 2: Trigger config sync, key distribution -``` - -**Funcionalidad Pendiente**: -- mDNS service advertisement -- Continuous peer monitoring -- Automatic config sync trigger -- TINC key distribution automation -- Peer health checks - -**Dependencies**: -- hashicorp/mdns v1.0.5 ✓ -- go.etcd.io/etcd/client/v3 v3.5.14 ✓ - -### Ansible - 10% Implementado -**Status**: ⚠️ Skeleton only (intencional) - -**Presente**: -- site.yml playbook structure -- ansible.cfg configuration -- inventory/hosts.ini template -- group_vars/all.yml variables -- Roles: bird/tasks/main.yml, tinc/tasks/main.yml - -**Implementación Actual**: -```yaml -# Todas las tasks son: -- name: Placeholder - debug: - msg: "Sprint 2 implementation" -``` - -**Propósito**: Sprint 1 usa Docker, Ansible para bare metal en Sprint 2+ - -**Syntax**: ✅ Válida (`make validate` passing) - -### Tests - 80% Implementados -**Status**: ⚠️ Funcionales pero algunos failing - -**Test Suite**: -```bash -✅ test_env_vars.sh # Validates .env variables -✅ test_configs.sh # Template rendering -✅ test_docker_builds.sh # Docker image builds -⚠️ test_bgp_peering.sh # BGP sessions (failing) -✅ test_full_stack.sh # E2E workflow -``` - -**Pass Rate**: 60% (3/5 core tests passing) - -**Failures**: -- BGP peering test: Expected "Established", got "Active" -- Ping tests over TINC: No connectivity - -**Passing**: -- Environment validation -- Config rendering -- Docker builds -- etcd cluster health -- TINC interface existence - ---- - -## 📊 Métricas Sprint 1 - -### Targets vs Actual - -| Métrica | Target | Actual | Status | -|---------|--------|--------|--------| -| Deploy time | <5min | ~2min | ✅ PASS | -| BGP sessions | Established | Active (blocked) | ❌ FAIL | -| TINC mesh | Connected | Interfaces only | ⚠️ PARTIAL | -| etcd latency | <10ms | 3.65ms | ✅ PASS | -| Containers up | 9/9 | 9/9 | ✅ PASS | -| Test pass rate | >80% | 60% | ⚠️ BELOW | -| Convergence | <120s | N/A | ⚠️ BLOCKED | - -### Optimización Files - -| Categoría | Plan Original | Actual | Optimización | -|-----------|---------------|--------|--------------| -| Core files | 42-45 | 28-30 | 33% reducción | -| Lines of code | ~3000 | ~2000 | Simplificado | -| Memory footprint | <2GB | ~1.5GB | ✅ Target met | - -### Container Resources - -``` -CONTAINER CPU% MEM USAGE / LIMIT MEM% -bird1 0.01% 12.5MiB / 31.09GiB 0.04% -tinc1 0.00% 8.2MiB / 31.09GiB 0.03% -etcd1 0.50% 45.6MiB / 31.09GiB 0.14% -prometheus 0.20% 180MiB / 31.09GiB 0.56% - -Total: ~1.5GB (all containers combined) -``` - ---- - -## 🛣️ Roadmap & Decisiones Pendientes - -### Sprint 1 Completion - PRIORITARIO - -**Issue Crítico**: Resolver TINC mesh connectivity - -**Opción A - Script Manual** (Recomendado para MVP) -```bash -Ventajas: -- Implementación rápida (1-2h) -- Testing inmediato -- Valida arquitectura completa -- Permite proceder con Sprint 2 - -Desventajas: -- No escalable -- Manual operation -- No production-ready - -Timeline: 1-2 horas -Effort: Low -Risk: Low -``` - -**Opción B - Go Daemon Fast-Track** (Mejor a largo plazo) -```bash -Ventajas: -- Solución automatizada -- Production-ready -- Scalable a N nodos -- Implementa Sprint 2 goals - -Desventajas: -- Tiempo de desarrollo mayor -- Más testing requerido -- Complejidad higher - -Timeline: 4-6 horas -Effort: Medium -Risk: Medium -``` - -**Decisión Requerida**: ¿Opción A para validar MVP rápido y luego Opción B, o directamente Opción B? - -### Sprint 2 Roadmap - -**Core Features** (must-have): -1. **Go Daemon Phase 2** - - mDNS service advertisement - - Peer discovery implementation - - Automated TINC key distribution - - Config sync on etcd watch trigger - - Peer health monitoring - -2. **Ansible Production Deployment** - - bird role: apt install, systemd unit, config template - - tinc role: install, keygen, mesh join automation - - etcd role: cluster bootstrap with Raft - - Integration with Docker configs - -3. **TINC Key Rotation** - - Automated key refresh (30-90 days) - - Zero-downtime rotation - - etcd-backed key storage - -**Nice-to-Have**: -- Custom Grafana dashboards -- BIRD exporter integration -- Enhanced logging (structured JSON) -- CI/CD GitHub Actions expansion - -**Timeline**: 2-3 semanas - -### Sprint 3-4 Features - -**Sprint 3 - Production Hardening**: -- Rolling updates (zero-downtime) -- Chaos testing (random container kills) -- BFD integration (fast failover <30s) -- Systemd units for bare metal -- Ansible Vault for secrets -- Multi-environment support (dev/staging/prod) - -**Sprint 4 - Scalability**: -- Route reflectors (>10 nodes) -- RPKI validation -- Multi-region etcd clustering -- BGP TCP-AO authentication -- Scalability testing (50-100 nodes) - ---- - -## ❓ Preguntas para Grok (Roadmap Planning) - -### 1. TINC Mesh Strategy -**Contexto**: TINC mesh bloqueado, 2 opciones para resolver. - -**Pregunta**: ¿Implementar script manual rápido (Opción A) para validar MVP y luego Go daemon automatizado (Opción B), o directamente fast-track Opción B? - -**Trade-offs**: -- Opción A: MVP validation en 1-2h, pero no production-ready -- Opción B: 4-6h implementación, pero automático y escalable -- Híbrido: A para testing, B para Sprint 2 - -**Recomendación**: Híbrido - script manual para desbloquear testing BGP, luego Go daemon en Sprint 2. - -### 2. Sprint Boundaries -**Contexto**: Sprint 1 tiene containers funcionando, etcd operacional, pero BGP bloqueado. - -**Pregunta**: ¿Es aceptable considerar Sprint 1 "completo con issues conocidos" y proceder a Sprint 2, o extender Sprint 1 hasta que BGP sessions establezcan? - -**Criterios Success**: -- Original: "BGP sessions established" -- Actual: "Infrastructure deployed, known blockers documented" - -**Recomendación**: Extender Sprint 1 solo el tiempo necesario para resolver TINC (1-2h con script manual), luego Sprint 2. - -### 3. Production Timeline -**Contexto**: Actualmente Docker-based, Ansible skeleton presente. - -**Pregunta**: ¿Cuándo se necesita deployment a bare metal / OpenWrt? Afecta prioridad de Ansible en Sprint 2. - -**Opciones**: -- Inmediato (Sprint 2): Priorizar Ansible roles -- Mediano plazo (Sprint 3): Focus en automation primero -- Largo plazo (Sprint 4): Perfeccionar Docker primero - -**Impacto**: Determina scope de Sprint 2. - -### 4. Scalability Testing -**Contexto**: Actualmente 3 nodos, arquitectura diseñada para >10. - -**Pregunta**: ¿En qué sprint testear beyond 3 nodes? ¿Cuántos nodos target (10, 50, 100)? - -**Opciones**: -- Sprint 2: 5 nodes (validar automation) -- Sprint 3: 10 nodes (validar RR-less scaling) -- Sprint 4: 50+ nodes (validar route reflectors) - -**Recomendación**: Sprint 2 con 5 nodes para validar Go daemon scaling. - -### 5. Monitoring Requirements -**Contexto**: Prometheus + Grafana corriendo, dashboards pending. - -**Pregunta**: ¿Qué métricas son críticas para Sprint 2 vs nice-to-have? - -**Críticas** (propuesta): -- BGP session status (up/down) -- TINC connections count -- etcd cluster health -- Route count per peer - -**Nice-to-Have**: -- Traffic graphs -- BGP convergence time -- TINC latency histograms -- etcd operation latency - -### 6. Testing Strategy -**Contexto**: 60% test pass rate, algunos tests blocked. - -**Pregunta**: ¿Nivel de cobertura target para Sprint 2? - -**Opciones**: -- Basic: >80% pass rate, integration tests functional -- Intermediate: + unit tests para Go daemon, chaos testing -- Comprehensive: + E2E automated, performance regression tests - -**Recomendación**: Intermediate para Sprint 2. - ---- - -## 📁 Archivos Clave del Proyecto - -### Configuración -- [`configs/bird/bird.conf.j2`](configs/bird/bird.conf.j2) - BIRD main template -- [`configs/bird/protocols.conf`](configs/bird/protocols.conf) - BGP peer definitions -- [`configs/tinc/tinc.conf.j2`](configs/tinc/tinc.conf.j2) - TINC mesh config -- [`docker-compose.yml`](docker-compose.yml) - 9 services orchestration -- [`.env.example`](.env.example) - Environment variables - -### Entrypoints -- [`docker/bird/entrypoint.sh`](docker/bird/entrypoint.sh) - BIRD startup + rendering -- [`docker/tinc/entrypoint.sh`](docker/tinc/entrypoint.sh) - TINC startup + keygen - -### Código -- [`daemon-go/cmd/bgp-daemon/main.go`](daemon-go/cmd/bgp-daemon/main.go) - Daemon principal -- [`daemon-go/pkg/discovery/mdns.go`](daemon-go/pkg/discovery/mdns.go) - mDNS discovery - -### Documentación -- [`CLAUDE.md`](CLAUDE.md) - Development guidelines (100% complete) -- [`README.md`](README.md) - Project overview + quick commands -- [`docs/QUICKSTART.md`](docs/QUICKSTART.md) - Setup instructions -- [`docs/architecture/decisions.md`](docs/architecture/decisions.md) - ADRs 1-7 - -### Tests -- [`tests/validation/test_env_vars.sh`](tests/validation/test_env_vars.sh) - ✅ Passing -- [`tests/integration/test_bgp_peering.sh`](tests/integration/test_bgp_peering.sh) - ❌ Failing (BGP) - ---- - -## 🔧 Comandos Útiles (Reference) - -### Verificar Estado -```bash -# Container status -make status -docker ps - -# BIRD protocols -docker exec bird1 birdc show protocols -docker exec bird1 birdc show protocols all peer1 - -# TINC interface -docker exec tinc1 ip addr show tinc0 -docker exec tinc1 ip route - -# etcd cluster -docker exec etcd1 etcdctl member list -docker exec etcd1 etcdctl endpoint health - -# Logs -docker logs -f bird1 -docker logs --tail 50 tinc1 -``` - -### Debugging TINC -```bash -# Check if tincd is running -docker exec tinc1 ps aux | grep tincd - -# View TINC config -docker exec tinc1 cat /var/run/tinc/bgpmesh/tinc.conf - -# Check host files -docker exec tinc1 ls -la /var/run/tinc/bgpmesh/hosts/ - -# View public key -docker exec tinc1 cat /var/run/tinc/bgpmesh/rsa_key.pub -``` - -### Testing -```bash -# Fast validation tests -make test-fast - -# Integration tests -make test-integration - -# All tests -make test-all -``` - ---- - -## 📝 Próximos Pasos Concretos - -### Inmediatos (1-2 días) -1. ✅ **Compartir este documento con Grok** -2. ⏳ **Decidir Opción A vs B para TINC mesh** -3. ⏳ **Implementar resolución de TINC** (1-6h según opción) -4. ⏳ **Validar BGP sessions establish** (después de TINC fix) -5. ⏳ **Cerrar Sprint 1** con documentación final - -### Sprint 2 (2-3 semanas) -1. Go daemon mDNS implementation -2. Automated TINC key distribution -3. Ansible roles (bird + tinc) -4. 5-node scalability test -5. Custom Grafana dashboards -6. CI/CD enhancement - -### Sprint 3-4 (1-2 meses) -1. Production hardening (BFD, rolling updates) -2. Chaos testing framework -3. Route reflectors (>10 nodes) -4. Multi-region etcd -5. RPKI validation - ---- - -## 📞 Información de Contacto - -**Repositorio**: `/home/pablo/repos/BGP` -**Branch Actual**: `master` -**Última Actualización**: 2025-10-28 - -**Para continuar desarrollo**: -1. Review este documento con Grok -2. Tomar decisiones sobre preguntas planteadas -3. Implementar resolución TINC mesh -4. Proceder con Sprint 2 roadmap - ---- - -**Generado con Claude Code para Grok Roadmap Planning** diff --git a/STATUS-SPRINT2-PHASE1.md b/STATUS-SPRINT2-PHASE1.md deleted file mode 100644 index 2f1e572..0000000 --- a/STATUS-SPRINT2-PHASE1.md +++ /dev/null @@ -1,308 +0,0 @@ -# Sprint 2 Phase 1 Status Report - -**Date**: 2025-10-28 -**Status**: COMPLETED - ---- - -## Summary - -Sprint 2 Phase 1 completado exitosamente con 4 deliverables principales: - -- **Testing Infrastructure**: Makefile + CI workflow con coverage enforcement -- **Unit Tests**: pkg/tinc (92.7%), pkg/discovery (89.8%), pkg/types (100%) -- **Docker Scaling**: 5-node deployment (15 containers, etcd quorum) -- **Ansible Automation**: 4 production-ready roles -- **Documentation**: Testing, Deployment guides, Status report - -**Test coverage**: 92.7% avg on library packages (target >70%) - ---- - -## Task 1: Makefile + CI Workflow - -### Makefile Implementation - -**File**: `daemon-go/Makefile` - -**Targets** (20+): -- **Test**: `test`, `test-unit`, `test-race`, `test-coverage`, `test-integration` -- **Build**: `build`, `build-race`, `install` -- **Quality**: `vet`, `fmt`, `lint` -- **Deps**: `deps`, `deps-tidy`, `deps-update` -- **CI**: `ci-test` (vet + race + coverage) -- **Dev**: `watch`, `coverage-html`, `clean`, `help` - -**Usage**: -```bash -cd daemon-go -make test-coverage # Run with coverage -make ci-test # Full CI suite -make build # Build binary -``` - -### CI Workflow Update - -**File**: `.github/workflows/ci.yml` - -**Changes**: -- Go version: 1.21 → 1.23 -- Added: `make deps`, `make vet`, `make test-unit`, `make test-race`, `make test-coverage` -- Added: gofmt validation -- Added: Coverage check (warns if <70%, doesn't fail in Phase 1) -- Added: Codecov integration (optional, continue-on-error) - -**Pipeline**: -1. Validate (env, YAML) -2. Build (3 Docker images) -3. Test Go (vet, fmt, unit, race, coverage) -4. Integration (deploy + test) - -**Duration**: ~8-12 minutes - -### Test Coverage Results - -``` -pkg/types: 100.0% (3 functions, 2 methods) -pkg/tinc: 92.7% (6 methods, 11 test functions) -pkg/discovery: 89.8% (4 functions, 9 test functions) -pkg/metrics: N/A (no testable statements) -cmd/bgp-daemon: 0.0% (main package, not typically tested) -``` - -**Overall**: 94.2% avg on library packages (exceeds 70% target) -**Total**: 37.2% with main (cmd/bgp-daemon brings down average) - ---- - -## Task 2: Docker 5-Node Scaling - -### Services Added - -**New containers** (6): -- bird4, bird5 (BGP routing) -- daemon4, daemon5 (Go automation) -- tinc4, tinc5 (VPN mesh) -- etcd4, etcd5 (distributed storage) - -**Total**: 15 containers (previously 9) - -### Configuration - -**IPs**: -- node4: 10.0.0.4 -- node5: 10.0.0.5 - -**Ports**: -- tinc4: 656:655/udp -- tinc5: 657:655/udp - -**etcd cluster**: -- 5-node quorum (tolerates 2 failures) -- Cluster string updated in all etcd1-3 configs -- All daemons updated with 5-node endpoints - -**Prometheus**: -- Updated scrape configs for all jobs (bird, tinc, etcd, bgp-daemon) -- 5 targets each: tinc1-5:2112, etcd1-5:2379, etc. - -### Resource Usage - -| Metric | 3-Node | 5-Node | -|--------|--------|--------| -| Containers | 9 | 15 | -| Memory | ~1.5GB | ~2.5GB | -| etcd Quorum | 2/3 | 3/5 | - ---- - -## Task 3: Ansible Roles - -### Role 1: etcd - -**Files**: -- `tasks/main.yml`: Install etcd v3.5.14, create user/dirs, systemd service -- `templates/etcd.conf.j2`: Environment-based configuration -- `templates/etcd.service.j2`: Systemd unit -- `handlers/main.yml`: reload systemd, restart etcd -- `defaults/main.yml`: etcd_version, etcd_cluster_token - -**Functionality**: -- Downloads etcd from GitHub releases -- Creates etcd user and /var/lib/etcd -- Templates configuration with cluster members -- Systemd service with notify type - -### Role 2: tinc - -**Files**: -- `tasks/main.yml`: Install TINC, generate RSA-4096 keys, etcd integration -- `templates/tinc.conf.j2`: Network config (mode switch, AES-256, peers) -- `templates/tinc-up.j2`: Interface setup, IP config, etcd propagation -- `templates/tinc-down.j2`: Interface teardown, etcd cleanup -- `templates/host.j2`: Local host file -- `handlers/main.yml`: restart tinc -- `defaults/main.yml`: tinc_netname, tinc_port, tinc_mtu - -**Functionality**: -- Installs tinc from apt -- Generates RSA-4096 keypair -- Stores public key in etcd (`/tinc/keys/`) -- Fetches peer keys from etcd -- Creates tinc-up/down scripts with IP configuration - -**Dependencies**: etcd role - -### Role 3: bird - -**Files**: -- `tasks/main.yml`: Install bird2, template configs, systemd override -- `templates/bird.conf.j2`: Main config (router-id, kernel protocol) -- `templates/protocols.conf.j2`: BGP peer definitions (dynamic from inventory) -- `templates/bird-override.conf.j2`: ExecStartPre (wait for tinc0) -- `handlers/main.yml`: reload systemd, restart/reload bird -- `defaults/main.yml`: bgp_as, bgp_bfd_enabled - -**Functionality**: -- Installs bird2 from apt -- Templates main config with router ID -- Generates BGP peer configs from inventory -- Validates config with `bird -p -c` -- Systemd override waits for TINC interface - -**Dependencies**: tinc role - -### Role 4: bgp-daemon - -**Files**: -- `tasks/main.yml`: Create user, copy binary, systemd service -- `templates/bgp-daemon.service.j2`: Systemd unit with security hardening -- `templates/bgp-daemon.env.j2`: Environment variables -- `handlers/main.yml`: reload systemd, restart bgp-daemon -- `defaults/main.yml`: bgp_daemon_binary_path - -**Functionality**: -- Creates bgp-daemon system user -- Copies Go binary to /opt/bgp-daemon/ -- Templates systemd service with: - - Security: PrivateTmp, NoNewPrivileges, ProtectSystem=strict - - ReadWritePaths: /var/run/tinc only -- Daemon arguments: -node, -tinc-net, -etcd, -iface, -metrics-addr - -**Dependencies**: etcd, tinc roles - -### Inventory Structure - -**Files**: -- `inventory/hosts.ini.example`: 5-node inventory template -- `inventory/group_vars/bgp_nodes.yml`: Dynamic variables -- `group_vars/all.yml`: Global settings - -**Dynamic variables**: -- `etcd_cluster_members`: Generated from inventory -- `etcd_endpoints`: Generated from inventory -- `tinc_peers`: All nodes except self -- `bgp_peers`: All nodes except self with IPs - -### Playbook - -**File**: `ansible/playbook.yml` - -**Workflow**: -1. **Pre-tasks**: apt update, install common deps, display info -2. **Roles**: etcd → tinc → bird → bgp-daemon (in order) -3. **Post-tasks**: Wait for etcd health, display service status - -**Usage**: -```bash -ansible-playbook -i inventory/hosts.ini playbook.yml # Full deploy -ansible-playbook -i inventory/hosts.ini playbook.yml --tags tinc # Specific role -ansible-playbook -i inventory/hosts.ini playbook.yml --limit node1 # Specific host -``` - ---- - -## Task 4: Documentation - -### Files Created - -- `docs/TESTING.md`: Unit tests, integration tests, CI/CD, troubleshooting -- `docs/DEPLOYMENT.md`: Docker deployment, Ansible deployment, scaling, backup/restore -- `STATUS-SPRINT2-PHASE1.md`: This document -- `README.md`: Updated Sprint Status section - -**Style**: Technical, concise, command-focused, no unnecessary explanations - ---- - -## Metrics - -| Metric | Value | -|--------|-------| -| Test coverage (lib) | 94.2% avg | -| Test functions | 20 | -| Docker containers | 15 | -| Memory (5-node) | ~2.5GB | -| CI duration | ~8-12 min | -| Ansible roles | 4 | -| New files | 44 | -| Modified files | 7 | - ---- - -## Known Issues - -1. **TINC key distribution**: Manual bootstrap required for initial deployment - - **Workaround**: Run tinc-bootstrap script after first deploy - - **Fix**: Sprint 2 Phase 2 will automate via Go daemon - -2. **Ansible first run**: Requires SSH key distribution - - **Workaround**: Use `ssh-copy-id` before running playbook - - **Documented**: In DEPLOYMENT.md - ---- - -## Completion Checklist - -- [x] Create daemon-go/Makefile (20+ targets) -- [x] Update .github/workflows/ci.yml (Go 1.23, coverage check) -- [x] Extend docker-compose.yml to 5 nodes (15 containers) -- [x] Update Prometheus scrape configs for 5 nodes -- [x] Create Ansible role: etcd -- [x] Create Ansible role: tinc -- [x] Create Ansible role: bird -- [x] Create Ansible role: bgp-daemon -- [x] Create Ansible inventory and playbook -- [x] Create docs/TESTING.md -- [x] Create docs/DEPLOYMENT.md -- [x] Create STATUS-SPRINT2-PHASE1.md -- [x] Update README.md -- [x] Create unit tests for pkg/tinc (11 test functions, 92.7% coverage) -- [x] Create unit tests for pkg/discovery (9 test functions, 89.8% coverage) - -**Status**: 15/15 tasks complete - ---- - -## Sprint 2 Phase 2 Roadmap - -**Priority**: -- Custom Grafana dashboards (BGP sessions, TINC connectivity, etcd health) -- Automated TINC key distribution via Go daemon -- Additional integration tests (5-node convergence, key distribution) - -**Nice-to-have**: -- Performance benchmarks (BGP convergence, TINC latency) -- Chaos testing framework (node failures, network partitions) -- cmd/bgp-daemon unit tests (main package refactoring) - ---- - -## References - -- Makefile: `daemon-go/Makefile` -- CI workflow: `.github/workflows/ci.yml` -- Docker config: `docker-compose.yml` -- Ansible roles: `ansible/roles/{etcd,tinc,bird,bgp-daemon}/` -- Testing guide: `docs/TESTING.md` -- Deployment guide: `docs/DEPLOYMENT.md` diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg deleted file mode 100644 index 0256f84..0000000 --- a/ansible/ansible.cfg +++ /dev/null @@ -1,13 +0,0 @@ -[defaults] -roles_path = roles -retry_files_enabled = false -host_key_checking = false -inventory = inventory/hosts.ini -forks = 10 -timeout = 30 - -[inventory] -enable_plugins = ini - -[ssh_connection] -pipelining = true diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml deleted file mode 100644 index 05c6161..0000000 --- a/ansible/group_vars/all.yml +++ /dev/null @@ -1,23 +0,0 @@ ---- -# Global variables for BGP network - -# TINC VPN settings -tinc_netname: "bgpmesh" -tinc_port: 655 -tinc_subnet_mask: 24 -tinc_mtu: 1400 - -# BGP settings -bgp_as: 65000 -bgp_bfd_enabled: false - -# etcd cluster settings -etcd_version: "3.5.14" -etcd_cluster_token: "bgp-mesh-cluster" - -# Monitoring -prometheus_port: 9090 -grafana_port: 3000 - -# Node-specific settings (will be overridden by host_vars) -node_name: "{{ inventory_hostname }}" diff --git a/ansible/inventory/group_vars/bgp_nodes.yml b/ansible/inventory/group_vars/bgp_nodes.yml deleted file mode 100644 index 949ec1b..0000000 --- a/ansible/inventory/group_vars/bgp_nodes.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -# Variables specific to BGP nodes - -# etcd cluster configuration -etcd_cluster_members: "{% for host in groups['etcd_cluster'] %}{{ host }}=http://{{ hostvars[host]['node_ip'] }}:2380{% if not loop.last %},{% endif %}{% endfor %}" -etcd_endpoints: "{% for host in groups['etcd_cluster'] %}{{ hostvars[host]['node_ip'] }}:2379{% if not loop.last %},{% endif %}{% endfor %}" - -# TINC peer list (all nodes except self) -tinc_peers: "{{ groups['tinc_mesh'] | difference([inventory_hostname]) }}" - -# BGP peer list (all nodes except self) -bgp_peers: "{% set peers = [] %}{% for host in groups['bird_routers'] if host != inventory_hostname %}{% set _ = peers.append({'name': host, 'ip': hostvars[host]['node_ip']}) %}{% endfor %}{{ peers }}" diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini deleted file mode 100644 index 069494a..0000000 --- a/ansible/inventory/hosts.ini +++ /dev/null @@ -1,20 +0,0 @@ -[birds] -bird1 ansible_host=localhost ansible_connection=local -bird2 ansible_host=localhost ansible_connection=local -bird3 ansible_host=localhost ansible_connection=local - -[tincs] -tinc1 ansible_host=localhost ansible_connection=local -tinc2 ansible_host=localhost ansible_connection=local -tinc3 ansible_host=localhost ansible_connection=local - -[etcd] -etcd1 ansible_host=localhost ansible_connection=local -etcd2 ansible_host=localhost ansible_connection=local -etcd3 ansible_host=localhost ansible_connection=local - -[monitoring] -prometheus ansible_host=localhost ansible_connection=local - -[all:vars] -ansible_python_interpreter=/usr/bin/python3 diff --git a/ansible/inventory/hosts.ini.example b/ansible/inventory/hosts.ini.example deleted file mode 100644 index d8f91bd..0000000 --- a/ansible/inventory/hosts.ini.example +++ /dev/null @@ -1,22 +0,0 @@ -# BGP Network Inventory Example -# Copy to hosts.ini and customize for your environment - -[bgp_nodes] -node1 ansible_host=192.168.1.101 node_ip=10.0.0.1 router_id=192.0.2.1 -node2 ansible_host=192.168.1.102 node_ip=10.0.0.2 router_id=192.0.2.2 -node3 ansible_host=192.168.1.103 node_ip=10.0.0.3 router_id=192.0.2.3 -node4 ansible_host=192.168.1.104 node_ip=10.0.0.4 router_id=192.0.2.4 -node5 ansible_host=192.168.1.105 node_ip=10.0.0.5 router_id=192.0.2.5 - -[bgp_nodes:vars] -ansible_user=root -ansible_python_interpreter=/usr/bin/python3 - -[etcd_cluster:children] -bgp_nodes - -[tinc_mesh:children] -bgp_nodes - -[bird_routers:children] -bgp_nodes diff --git a/ansible/playbook.yml b/ansible/playbook.yml deleted file mode 100644 index 953c50a..0000000 --- a/ansible/playbook.yml +++ /dev/null @@ -1,73 +0,0 @@ ---- -# Main deployment playbook for BGP network -# Usage: ansible-playbook -i inventory/hosts.ini playbook.yml - -- name: Deploy BGP Network Infrastructure - hosts: bgp_nodes - become: yes - gather_facts: yes - - pre_tasks: - - name: Update apt cache - apt: - update_cache: yes - cache_valid_time: 3600 - - - name: Install common dependencies - apt: - name: - - python3 - - python3-pip - - curl - - vim - - htop - - tcpdump - state: present - - - name: Display deployment information - debug: - msg: | - Deploying BGP node: {{ inventory_hostname }} - Node IP: {{ node_ip }} - Router ID: {{ router_id }} - BGP AS: {{ bgp_as }} - TINC Network: {{ tinc_netname }} - etcd Cluster: {{ etcd_cluster_members }} - - roles: - - role: etcd - tags: ['etcd'] - - - role: tinc - tags: ['tinc', 'vpn'] - - - role: bird - tags: ['bird', 'bgp'] - - - role: bgp-daemon - tags: ['daemon', 'automation'] - - post_tasks: - - name: Wait for etcd to be healthy - command: etcdctl endpoint health --endpoints={{ etcd_endpoints }} - register: etcd_health - until: etcd_health.rc == 0 - retries: 10 - delay: 5 - tags: ['verify'] - - - name: Display service status - shell: | - echo "=== Service Status ===" - systemctl status etcd --no-pager -l || true - systemctl status tinc@{{ tinc_netname }} --no-pager -l || true - systemctl status bird --no-pager -l || true - systemctl status bgp-daemon --no-pager -l || true - register: service_status - changed_when: false - tags: ['verify'] - - - name: Show service status - debug: - var: service_status.stdout_lines - tags: ['verify'] diff --git a/ansible/roles/bgp-daemon/defaults/main.yml b/ansible/roles/bgp-daemon/defaults/main.yml deleted file mode 100644 index fc70e66..0000000 --- a/ansible/roles/bgp-daemon/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -bgp_daemon_binary_path: "../daemon-go/bin/bgp-daemon" diff --git a/ansible/roles/bgp-daemon/handlers/main.yml b/ansible/roles/bgp-daemon/handlers/main.yml deleted file mode 100644 index ae3bb5e..0000000 --- a/ansible/roles/bgp-daemon/handlers/main.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- name: reload systemd - systemd: - daemon_reload: yes - -- name: restart bgp-daemon - systemd: - name: bgp-daemon - state: restarted diff --git a/ansible/roles/bgp-daemon/meta/main.yml b/ansible/roles/bgp-daemon/meta/main.yml deleted file mode 100644 index f4615a7..0000000 --- a/ansible/roles/bgp-daemon/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -dependencies: - - role: etcd - - role: tinc diff --git a/ansible/roles/bgp-daemon/tasks/main.yml b/ansible/roles/bgp-daemon/tasks/main.yml deleted file mode 100644 index 7ac3ed7..0000000 --- a/ansible/roles/bgp-daemon/tasks/main.yml +++ /dev/null @@ -1,55 +0,0 @@ ---- -- name: Create bgp-daemon user - user: - name: bgp-daemon - system: yes - create_home: no - shell: /usr/sbin/nologin - -- name: Create bgp-daemon directories - file: - path: "{{ item }}" - state: directory - owner: bgp-daemon - group: bgp-daemon - mode: '0755' - loop: - - /opt/bgp-daemon - - /etc/bgp-daemon - - /var/log/bgp-daemon - -- name: Copy bgp-daemon binary - copy: - src: "{{ bgp_daemon_binary_path | default('../daemon-go/bin/bgp-daemon') }}" - dest: /opt/bgp-daemon/bgp-daemon - owner: bgp-daemon - group: bgp-daemon - mode: '0755' - notify: restart bgp-daemon - -- name: Create bgp-daemon environment file - template: - src: bgp-daemon.env.j2 - dest: /etc/bgp-daemon/daemon.env - owner: bgp-daemon - group: bgp-daemon - mode: '0600' - notify: restart bgp-daemon - -- name: Create bgp-daemon systemd service - template: - src: bgp-daemon.service.j2 - dest: /etc/systemd/system/bgp-daemon.service - owner: root - group: root - mode: '0644' - notify: - - reload systemd - - restart bgp-daemon - -- name: Enable and start bgp-daemon service - systemd: - name: bgp-daemon - enabled: yes - state: started - daemon_reload: yes diff --git a/ansible/roles/bgp-daemon/templates/bgp-daemon.env.j2 b/ansible/roles/bgp-daemon/templates/bgp-daemon.env.j2 deleted file mode 100644 index 97f3d54..0000000 --- a/ansible/roles/bgp-daemon/templates/bgp-daemon.env.j2 +++ /dev/null @@ -1,4 +0,0 @@ -# Environment variables for bgp-daemon -NODE_NAME={{ node_name }} -TINC_NETNAME={{ tinc_netname }} -ETCD_ENDPOINTS={{ etcd_endpoints }} diff --git a/ansible/roles/bgp-daemon/templates/bgp-daemon.service.j2 b/ansible/roles/bgp-daemon/templates/bgp-daemon.service.j2 deleted file mode 100644 index 3d7905e..0000000 --- a/ansible/roles/bgp-daemon/templates/bgp-daemon.service.j2 +++ /dev/null @@ -1,33 +0,0 @@ -[Unit] -Description=BGP Propagation Daemon -Documentation=https://github.com/pablomonte/bgp-network -After=network.target etcd.service tinc@{{ tinc_netname }}.service -Requires=etcd.service tinc@{{ tinc_netname }}.service - -[Service] -Type=simple -User=bgp-daemon -Group=bgp-daemon -EnvironmentFile=/etc/bgp-daemon/daemon.env -ExecStart=/opt/bgp-daemon/bgp-daemon \ - -node={{ node_name }} \ - -tinc-net={{ tinc_netname }} \ - -etcd={{ etcd_endpoints }} \ - -iface=tinc0 \ - -metrics-addr=:2112 \ - -v -Restart=on-failure -RestartSec=5s -StandardOutput=journal -StandardError=journal -SyslogIdentifier=bgp-daemon - -# Security hardening -PrivateTmp=yes -NoNewPrivileges=yes -ProtectSystem=strict -ProtectHome=yes -ReadWritePaths=/var/run/tinc - -[Install] -WantedBy=multi-user.target diff --git a/ansible/roles/bird/defaults/main.yml b/ansible/roles/bird/defaults/main.yml deleted file mode 100644 index b9a374f..0000000 --- a/ansible/roles/bird/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -bgp_as: 65000 -bgp_bfd_enabled: false diff --git a/ansible/roles/bird/handlers/main.yml b/ansible/roles/bird/handlers/main.yml deleted file mode 100644 index 415d918..0000000 --- a/ansible/roles/bird/handlers/main.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -- name: reload systemd - systemd: - daemon_reload: yes - -- name: restart bird - systemd: - name: bird - state: restarted - -- name: reload bird - systemd: - name: bird - state: reloaded diff --git a/ansible/roles/bird/meta/main.yml b/ansible/roles/bird/meta/main.yml deleted file mode 100644 index 3d653f1..0000000 --- a/ansible/roles/bird/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -dependencies: - - role: tinc diff --git a/ansible/roles/bird/tasks/main.yml b/ansible/roles/bird/tasks/main.yml deleted file mode 100644 index 1e10725..0000000 --- a/ansible/roles/bird/tasks/main.yml +++ /dev/null @@ -1,60 +0,0 @@ ---- -- name: Install BIRD BGP daemon - apt: - name: - - bird2 - state: present - update_cache: yes - -- name: Create BIRD configuration directory - file: - path: /etc/bird - state: directory - owner: root - group: root - mode: '0755' - -- name: Deploy BIRD main configuration - template: - src: bird.conf.j2 - dest: /etc/bird/bird.conf - owner: root - group: root - mode: '0644' - validate: 'bird -p -c %s' - notify: restart bird - -- name: Create BIRD protocols configuration - template: - src: protocols.conf.j2 - dest: /etc/bird/protocols.conf - owner: root - group: root - mode: '0644' - notify: restart bird - -- name: Create BIRD systemd override directory - file: - path: /etc/systemd/system/bird.service.d - state: directory - owner: root - group: root - mode: '0755' - -- name: Deploy BIRD systemd override - template: - src: bird-override.conf.j2 - dest: /etc/systemd/system/bird.service.d/override.conf - owner: root - group: root - mode: '0644' - notify: - - reload systemd - - restart bird - -- name: Enable and start BIRD service - systemd: - name: bird - enabled: yes - state: started - daemon_reload: yes diff --git a/ansible/roles/bird/templates/bird-override.conf.j2 b/ansible/roles/bird/templates/bird-override.conf.j2 deleted file mode 100644 index 1469a90..0000000 --- a/ansible/roles/bird/templates/bird-override.conf.j2 +++ /dev/null @@ -1,3 +0,0 @@ -[Service] -# Wait for TINC interface to be ready -ExecStartPre=/bin/sh -c 'until ip link show tinc0; do sleep 1; done' diff --git a/ansible/roles/bird/templates/bird.conf.j2 b/ansible/roles/bird/templates/bird.conf.j2 deleted file mode 100644 index 5925366..0000000 --- a/ansible/roles/bird/templates/bird.conf.j2 +++ /dev/null @@ -1,28 +0,0 @@ -# BIRD BGP configuration for {{ node_name }} -# Generated by Ansible - -router id {{ router_id }}; - -log syslog all; -debug protocols { states, routes, filters, interfaces, events }; - -# Device protocol - interface monitoring -protocol device { - scan time 10; -} - -# Kernel protocol - sync routes with kernel -protocol kernel { - ipv4 { - import none; - export all; - }; -} - -# Static routes -protocol static { - ipv4; -} - -# Include BGP peer configurations -include "/etc/bird/protocols.conf"; diff --git a/ansible/roles/bird/templates/protocols.conf.j2 b/ansible/roles/bird/templates/protocols.conf.j2 deleted file mode 100644 index a8b6447..0000000 --- a/ansible/roles/bird/templates/protocols.conf.j2 +++ /dev/null @@ -1,19 +0,0 @@ -# BGP peer configurations for {{ node_name }} - -{% for peer in bgp_peers %} -protocol bgp {{ peer.name }} { - description "BGP peer {{ peer.name }} at {{ peer.ip }}"; - local {{ node_ip }} as {{ bgp_as }}; - neighbor {{ peer.ip }} as {{ bgp_as }}; - - ipv4 { - import all; - export all; - }; - - {% if bgp_bfd_enabled | default(false) %} - bfd on; - {% endif %} -} - -{% endfor %} diff --git a/ansible/roles/etcd/defaults/main.yml b/ansible/roles/etcd/defaults/main.yml deleted file mode 100644 index 8d4b459..0000000 --- a/ansible/roles/etcd/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -etcd_version: "3.5.14" -etcd_cluster_token: "bgp-mesh-cluster" diff --git a/ansible/roles/etcd/handlers/main.yml b/ansible/roles/etcd/handlers/main.yml deleted file mode 100644 index 780d93f..0000000 --- a/ansible/roles/etcd/handlers/main.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- name: reload systemd - systemd: - daemon_reload: yes - -- name: restart etcd - systemd: - name: etcd - state: restarted diff --git a/ansible/roles/etcd/meta/main.yml b/ansible/roles/etcd/meta/main.yml deleted file mode 100644 index 23d65c7..0000000 --- a/ansible/roles/etcd/meta/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -dependencies: [] diff --git a/ansible/roles/etcd/tasks/main.yml b/ansible/roles/etcd/tasks/main.yml deleted file mode 100644 index cbdd6fe..0000000 --- a/ansible/roles/etcd/tasks/main.yml +++ /dev/null @@ -1,85 +0,0 @@ ---- -- name: Install etcd dependencies - apt: - name: - - curl - - tar - state: present - update_cache: yes - -- name: Create etcd user - user: - name: etcd - system: yes - create_home: no - shell: /usr/sbin/nologin - -- name: Create etcd directories - file: - path: "{{ item }}" - state: directory - owner: etcd - group: etcd - mode: '0755' - loop: - - /etc/etcd - - /var/lib/etcd - -- name: Check if etcd is installed - stat: - path: /usr/local/bin/etcd - register: etcd_binary - -- name: Download and extract etcd - when: not etcd_binary.stat.exists - block: - - name: Download etcd tarball - get_url: - url: "https://github.com/etcd-io/etcd/releases/download/v{{ etcd_version }}/etcd-v{{ etcd_version }}-linux-amd64.tar.gz" - dest: "/tmp/etcd-v{{ etcd_version }}-linux-amd64.tar.gz" - mode: '0644' - - - name: Extract etcd - unarchive: - src: "/tmp/etcd-v{{ etcd_version }}-linux-amd64.tar.gz" - dest: /tmp - remote_src: yes - - - name: Install etcd binaries - copy: - src: "/tmp/etcd-v{{ etcd_version }}-linux-amd64/{{ item }}" - dest: "/usr/local/bin/{{ item }}" - mode: '0755' - owner: root - group: root - remote_src: yes - loop: - - etcd - - etcdctl - -- name: Create etcd configuration - template: - src: etcd.conf.j2 - dest: /etc/etcd/etcd.conf - owner: etcd - group: etcd - mode: '0644' - notify: restart etcd - -- name: Create etcd systemd service - template: - src: etcd.service.j2 - dest: /etc/systemd/system/etcd.service - owner: root - group: root - mode: '0644' - notify: - - reload systemd - - restart etcd - -- name: Enable and start etcd service - systemd: - name: etcd - enabled: yes - state: started - daemon_reload: yes diff --git a/ansible/roles/etcd/templates/etcd.conf.j2 b/ansible/roles/etcd/templates/etcd.conf.j2 deleted file mode 100644 index 130c134..0000000 --- a/ansible/roles/etcd/templates/etcd.conf.j2 +++ /dev/null @@ -1,10 +0,0 @@ -# etcd configuration for {{ node_name }} -ETCD_NAME={{ node_name }} -ETCD_DATA_DIR=/var/lib/etcd -ETCD_LISTEN_CLIENT_URLS=http://0.0.0.0:2379 -ETCD_ADVERTISE_CLIENT_URLS=http://{{ node_ip }}:2379 -ETCD_LISTEN_PEER_URLS=http://0.0.0.0:2380 -ETCD_INITIAL_ADVERTISE_PEER_URLS=http://{{ node_ip }}:2380 -ETCD_INITIAL_CLUSTER={{ etcd_cluster_members | join(',') }} -ETCD_INITIAL_CLUSTER_STATE=new -ETCD_INITIAL_CLUSTER_TOKEN={{ etcd_cluster_token }} diff --git a/ansible/roles/etcd/templates/etcd.service.j2 b/ansible/roles/etcd/templates/etcd.service.j2 deleted file mode 100644 index fcf9828..0000000 --- a/ansible/roles/etcd/templates/etcd.service.j2 +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=etcd distributed key-value store -Documentation=https://etcd.io/docs/ -After=network.target - -[Service] -Type=notify -User=etcd -Group=etcd -EnvironmentFile=/etc/etcd/etcd.conf -ExecStart=/usr/local/bin/etcd -Restart=on-failure -RestartSec=5 -LimitNOFILE=65536 - -[Install] -WantedBy=multi-user.target diff --git a/ansible/roles/tinc/defaults/main.yml b/ansible/roles/tinc/defaults/main.yml deleted file mode 100644 index 25086e6..0000000 --- a/ansible/roles/tinc/defaults/main.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -tinc_netname: "bgpmesh" -tinc_port: 655 -tinc_subnet_mask: 24 -tinc_mtu: 1400 diff --git a/ansible/roles/tinc/handlers/main.yml b/ansible/roles/tinc/handlers/main.yml deleted file mode 100644 index c733f79..0000000 --- a/ansible/roles/tinc/handlers/main.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -- name: restart tinc - systemd: - name: "tinc@{{ tinc_netname }}" - state: restarted diff --git a/ansible/roles/tinc/meta/main.yml b/ansible/roles/tinc/meta/main.yml deleted file mode 100644 index ab7b63f..0000000 --- a/ansible/roles/tinc/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -dependencies: - - role: etcd diff --git a/ansible/roles/tinc/tasks/main.yml b/ansible/roles/tinc/tasks/main.yml deleted file mode 100644 index 7cc54dd..0000000 --- a/ansible/roles/tinc/tasks/main.yml +++ /dev/null @@ -1,94 +0,0 @@ ---- -- name: Install TINC VPN - apt: - name: - - tinc - state: present - update_cache: yes - -- name: Create TINC network directory - file: - path: "/etc/tinc/{{ tinc_netname }}/hosts" - state: directory - owner: root - group: root - mode: '0755' - -- name: Check if TINC keypair exists - stat: - path: "/etc/tinc/{{ tinc_netname }}/rsa_key.priv" - register: tinc_keypair - -- name: Generate TINC RSA keypair - command: "tincd -n {{ tinc_netname }} -K4096" - args: - creates: "/etc/tinc/{{ tinc_netname }}/rsa_key.priv" - when: not tinc_keypair.stat.exists - environment: - TINC_HOST_NAME: "{{ node_name }}" - -- name: Create TINC configuration - template: - src: tinc.conf.j2 - dest: "/etc/tinc/{{ tinc_netname }}/tinc.conf" - owner: root - group: root - mode: '0644' - notify: restart tinc - -- name: Create tinc-up script - template: - src: tinc-up.j2 - dest: "/etc/tinc/{{ tinc_netname }}/tinc-up" - owner: root - group: root - mode: '0755' - notify: restart tinc - -- name: Create tinc-down script - template: - src: tinc-down.j2 - dest: "/etc/tinc/{{ tinc_netname }}/tinc-down" - owner: root - group: root - mode: '0755' - notify: restart tinc - -- name: Create local host file - template: - src: host.j2 - dest: "/etc/tinc/{{ tinc_netname }}/hosts/{{ node_name }}" - owner: root - group: root - mode: '0644' - notify: restart tinc - -- name: Read local public key - slurp: - src: "/etc/tinc/{{ tinc_netname }}/rsa_key.pub" - register: tinc_public_key - when: etcd_endpoints is defined - -- name: Store public key in etcd - command: > - etcdctl put /tinc/keys/{{ node_name }} "{{ tinc_public_key.content | b64decode }}" - environment: - ETCDCTL_ENDPOINTS: "{{ etcd_endpoints }}" - when: etcd_endpoints is defined and tinc_public_key is defined - -- name: Fetch peer host files from etcd - shell: | - for peer in $(etcdctl get /tinc/keys/ --prefix --keys-only | grep -v "{{ node_name }}"); do - peer_name=$(basename $peer) - etcdctl get /tinc/keys/$peer_name > /etc/tinc/{{ tinc_netname }}/hosts/$peer_name - done - environment: - ETCDCTL_ENDPOINTS: "{{ etcd_endpoints }}" - when: etcd_endpoints is defined - notify: restart tinc - -- name: Enable and start TINC service - systemd: - name: "tinc@{{ tinc_netname }}" - enabled: yes - state: started diff --git a/ansible/roles/tinc/templates/host.j2 b/ansible/roles/tinc/templates/host.j2 deleted file mode 100644 index 819a7a3..0000000 --- a/ansible/roles/tinc/templates/host.j2 +++ /dev/null @@ -1,3 +0,0 @@ -Address = {{ ansible_default_ipv4.address }} -Port = {{ tinc_port }} -Compression = 11 diff --git a/ansible/roles/tinc/templates/tinc-down.j2 b/ansible/roles/tinc/templates/tinc-down.j2 deleted file mode 100644 index 74eb9fd..0000000 --- a/ansible/roles/tinc/templates/tinc-down.j2 +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -# TINC down script for {{ node_name }} - -# Remove from etcd -if command -v etcdctl >/dev/null 2>&1; then - etcdctl del /peers/{{ node_name }} --endpoints="{{ etcd_endpoints }}" -fi - -# Bring down interface -ip link set $INTERFACE down diff --git a/ansible/roles/tinc/templates/tinc-up.j2 b/ansible/roles/tinc/templates/tinc-up.j2 deleted file mode 100644 index 84e2f19..0000000 --- a/ansible/roles/tinc/templates/tinc-up.j2 +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -# TINC up script for {{ node_name }} - -# Bring up interface -ip link set $INTERFACE up - -# Configure IPv4 address -ip addr add {{ node_ip }}/{{ tinc_subnet_mask }} dev $INTERFACE - -# Configure IPv6 address (optional) -{% if tinc_ipv6 is defined %} -ip -6 addr add {{ tinc_ipv6 }}/64 dev $INTERFACE -{% endif %} - -# Set MTU -ip link set $INTERFACE mtu {{ tinc_mtu | default(1400) }} - -# Propagate node info to etcd -if command -v etcdctl >/dev/null 2>&1; then - # Read public key and escape for JSON - if [ -f "/etc/tinc/{{ tinc_netname }}/rsa_key.pub" ]; then - PUBLIC_KEY=$(cat /etc/tinc/{{ tinc_netname }}/rsa_key.pub | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g') - etcdctl put /peers/{{ node_name }} "{\"ip\":\"{{ node_ip }}\",\"endpoint\":\"{{ ansible_default_ipv4.address }}:{{ tinc_port }}\",\"key\":\"$PUBLIC_KEY\"}" --endpoints="{{ etcd_endpoints }}" - else - # Fallback: store without key if not available yet - etcdctl put /peers/{{ node_name }} "{\"ip\":\"{{ node_ip }}\",\"endpoint\":\"{{ ansible_default_ipv4.address }}:{{ tinc_port }}\"}" --endpoints="{{ etcd_endpoints }}" - fi -fi diff --git a/ansible/roles/tinc/templates/tinc.conf.j2 b/ansible/roles/tinc/templates/tinc.conf.j2 deleted file mode 100644 index 0a2dfa7..0000000 --- a/ansible/roles/tinc/templates/tinc.conf.j2 +++ /dev/null @@ -1,16 +0,0 @@ -# TINC configuration for {{ node_name }} -Name = {{ node_name }} -Mode = switch -Device = /dev/net/tun -Interface = tinc0 -Port = {{ tinc_port }} - -# Encryption -Cipher = aes-256-cbc -Digest = sha256 -Compression = 11 - -# Connect to peers -{% for peer in tinc_peers %} -ConnectTo = {{ peer }} -{% endfor %} diff --git a/ansible/site.yml b/ansible/site.yml deleted file mode 100644 index 1b714c3..0000000 --- a/ansible/site.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -# Main playbook for BGP network configuration -# Sprint 1: Skeleton only (using Docker) -# Sprint 2+: Full implementation for production deployment - -- name: Configure BGP network nodes - hosts: all - become: yes - gather_facts: yes - - roles: - - bird - - tinc - - tasks: - - name: Display deployment info - debug: - msg: | - BGP Network Configuration - ========================= - Host: {{ inventory_hostname }} - BGP AS: {{ bgp_as }} - TINC Network: {{ tinc_netname }} - - Note: Sprint 1 uses Docker containers. - Full Ansible deployment coming in Sprint 2. diff --git a/configs/bird/bird.conf.j2 b/configs/bird/bird.conf.j2 deleted file mode 100644 index 13100f6..0000000 --- a/configs/bird/bird.conf.j2 +++ /dev/null @@ -1,28 +0,0 @@ -# BIRD 2.x Configuration -# Generated from template - -router id {{ router_id }}; - -log syslog all; -debug protocols all; - -# Device protocol for interface tracking -protocol device { -} - -# Kernel protocol for IPv4 route synchronization -protocol kernel { - ipv4 { - import all; - export all; - }; -} - -# Static routes protocol -protocol static { - ipv4; -} - -# Include additional configurations -include "/etc/bird/protocols.conf"; -include "/etc/bird/filters.conf"; diff --git a/configs/bird/filters.conf b/configs/bird/filters.conf deleted file mode 100644 index 53fff53..0000000 --- a/configs/bird/filters.conf +++ /dev/null @@ -1,12 +0,0 @@ -# BGP Route Filters -# Sprint 1: Simplified filters for testing - -# Export filter: Accept all for Sprint 1 -filter export_bgp { - accept; -} - -# Import filter: Accept all for Sprint 1 -filter import_bgp { - accept; -} diff --git a/configs/bird/protocols-1.conf b/configs/bird/protocols-1.conf deleted file mode 100644 index 1cd801f..0000000 --- a/configs/bird/protocols-1.conf +++ /dev/null @@ -1,26 +0,0 @@ -# BGP Peer Configurations for bird1 (10.0.0.1) -# Peers over TINC mesh (10.0.0.0/24) - -# Peer to node2 -protocol bgp peer1 { - description "BGP peer at 10.0.0.2"; - local 10.0.0.1 as 65000; - neighbor 10.0.0.2 as 65000; - - ipv4 { - import all; - export all; - }; -} - -# Peer to node3 -protocol bgp peer2 { - description "BGP peer at 10.0.0.3"; - local 10.0.0.1 as 65000; - neighbor 10.0.0.3 as 65000; - - ipv4 { - import all; - export all; - }; -} diff --git a/configs/bird/protocols-2.conf b/configs/bird/protocols-2.conf deleted file mode 100644 index 11113d6..0000000 --- a/configs/bird/protocols-2.conf +++ /dev/null @@ -1,26 +0,0 @@ -# BGP Peer Configurations for bird2 (10.0.0.2) -# Peers over TINC mesh (10.0.0.0/24) - -# Peer to node1 -protocol bgp peer1 { - description "BGP peer at 10.0.0.1"; - local 10.0.0.2 as 65000; - neighbor 10.0.0.1 as 65000; - - ipv4 { - import all; - export all; - }; -} - -# Peer to node3 -protocol bgp peer2 { - description "BGP peer at 10.0.0.3"; - local 10.0.0.2 as 65000; - neighbor 10.0.0.3 as 65000; - - ipv4 { - import all; - export all; - }; -} diff --git a/configs/bird/protocols-3.conf b/configs/bird/protocols-3.conf deleted file mode 100644 index e396c5d..0000000 --- a/configs/bird/protocols-3.conf +++ /dev/null @@ -1,26 +0,0 @@ -# BGP Peer Configurations for bird3 (10.0.0.3) -# Peers over TINC mesh (10.0.0.0/24) - -# Peer to node1 -protocol bgp peer1 { - description "BGP peer at 10.0.0.1"; - local 10.0.0.3 as 65000; - neighbor 10.0.0.1 as 65000; - - ipv4 { - import all; - export all; - }; -} - -# Peer to node2 -protocol bgp peer2 { - description "BGP peer at 10.0.0.2"; - local 10.0.0.3 as 65000; - neighbor 10.0.0.2 as 65000; - - ipv4 { - import all; - export all; - }; -} diff --git a/configs/bird/protocols.conf b/configs/bird/protocols.conf deleted file mode 100644 index a05d934..0000000 --- a/configs/bird/protocols.conf +++ /dev/null @@ -1,27 +0,0 @@ -# BGP Peer Configurations -# Peers over TINC mesh (10.0.0.0/24) -# Sprint 1: Skeleton configuration - -# Peer 1 (TINC IP: 10.0.0.2) -protocol bgp peer1 { - description "BGP peer at 10.0.0.2"; - local 10.0.0.1 as 65000; - neighbor 10.0.0.2 as 65000; - - ipv4 { - import all; - export all; - }; -} - -# Peer 2 (TINC IP: 10.0.0.3) -protocol bgp peer2 { - description "BGP peer at 10.0.0.3"; - local 10.0.0.1 as 65000; - neighbor 10.0.0.3 as 65000; - - ipv4 { - import all; - export all; - }; -} diff --git a/configs/bird/protocols.conf.j2 b/configs/bird/protocols.conf.j2 deleted file mode 100644 index a83ed5f..0000000 --- a/configs/bird/protocols.conf.j2 +++ /dev/null @@ -1,28 +0,0 @@ -# BGP Peer Configurations -# Peers over TINC mesh (10.0.0.0/24) -# Dynamically generated from template -# -# Template variables: -# node_ip: This node's TINC IP (e.g., 10.0.0.1) -# node_id: This node's numeric ID (e.g., 1) -# bgp_as: BGP AS number (e.g., 65000) -# total_nodes: Total number of nodes in mesh (e.g., 5) -# -# Generated config creates N-1 BGP peers (full mesh topology) - -{% for peer_id in range(1, total_nodes + 1) %} -{% if peer_id != node_id %} -# Peer {{ loop.index }} (TINC IP: 10.0.0.{{ peer_id }}) -protocol bgp peer{{ loop.index }} { - description "BGP peer at 10.0.0.{{ peer_id }}"; - local {{ node_ip }} as {{ bgp_as }}; - neighbor 10.0.0.{{ peer_id }} as {{ bgp_as }}; - - ipv4 { - import all; - export all; - }; -} - -{% endif %} -{% endfor %} diff --git a/configs/etcd/etcd.conf b/configs/etcd/etcd.conf deleted file mode 100644 index c61abbe..0000000 --- a/configs/etcd/etcd.conf +++ /dev/null @@ -1,30 +0,0 @@ -# etcd Configuration -# Note: Most configuration is passed via command-line args in docker-compose.yml -# This file is a placeholder for future custom configurations - -# Client communication -# listen-client-urls: http://0.0.0.0:2379 -# advertise-client-urls: http://etcd1:2379 - -# Peer communication -# listen-peer-urls: http://0.0.0.0:2380 -# initial-advertise-peer-urls: http://etcd1:2380 - -# Cluster configuration -# initial-cluster: etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380 -# initial-cluster-state: new -# initial-cluster-token: bgp-etcd-cluster - -# Data directory -# data-dir: /etcd-data - -# Heartbeat and election -# heartbeat-interval: 100 -# election-timeout: 1000 - -# Snapshots -# snapshot-count: 10000 - -# WAL -# max-snapshots: 5 -# max-wals: 5 diff --git a/configs/grafana/dashboards/bgp-daemon-overview.json b/configs/grafana/dashboards/bgp-daemon-overview.json deleted file mode 100644 index b29914a..0000000 --- a/configs/grafana/dashboards/bgp-daemon-overview.json +++ /dev/null @@ -1,578 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 0, - "id": null, - "links": [], - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "yellow", - "value": 1 - }, - { - "color": "green", - "value": 2 - } - ] - }, - "unit": "none" - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 0, - "y": 0 - }, - "id": 1, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": [ - "lastNotNull" - ], - "fields": "" - }, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "11.2.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "bgp_daemon_peers_discovered", - "refId": "A", - "legendFormat": "{{instance}}" - } - ], - "title": "Peers Discovered (mDNS)", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 1 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 6, - "w": 8, - "x": 8, - "y": 0 - }, - "id": 5, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "values": false, - "calcs": [ - "lastNotNull" - ], - "fields": "" - }, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "11.2.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "bgp_daemon_etcd_watch_errors_total", - "refId": "A", - "legendFormat": "{{instance}}" - } - ], - "title": "etcd Watch Errors (Total)", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "ops" - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": ".*success.*" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "green", - "mode": "fixed" - } - } - ] - }, - { - "matcher": { - "id": "byRegexp", - "options": ".*error.*" - }, - "properties": [ - { - "id": "color", - "value": { - "fixedColor": "red", - "mode": "fixed" - } - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 6 - }, - "id": 2, - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "11.2.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "rate(bgp_daemon_peer_sync_total[5m])", - "refId": "A", - "legendFormat": "{{instance}} - {{status}}" - } - ], - "title": "Peer Sync Operations (rate/5m)", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "custom": { - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "scaleDistribution": { - "type": "linear" - } - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 6 - }, - "id": 3, - "options": { - "calculate": true, - "cellGap": 2, - "cellValues": {}, - "color": { - "exponent": 0.5, - "fill": "dark-orange", - "mode": "scheme", - "reverse": false, - "scale": "exponential", - "scheme": "Spectral", - "steps": 128 - }, - "exemplars": { - "color": "rgba(255,0,255,0.7)" - }, - "filterValues": { - "le": 1e-9 - }, - "legend": { - "show": true - }, - "rowsFrame": { - "layout": "auto" - }, - "tooltip": { - "mode": "single", - "showColorScale": false, - "yHistogram": false - }, - "yAxis": { - "axisPlacement": "left", - "reverse": false, - "unit": "s", - "decimals": 3 - } - }, - "pluginVersion": "11.2.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "rate(bgp_daemon_tinc_reload_duration_seconds_bucket[5m])", - "format": "heatmap", - "refId": "A", - "legendFormat": "{{le}}" - } - ], - "title": "TINC Reload Duration Distribution", - "type": "heatmap" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "custom": { - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "scaleDistribution": { - "type": "linear" - } - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 14 - }, - "id": 4, - "options": { - "calculate": true, - "cellGap": 2, - "cellValues": {}, - "color": { - "exponent": 0.5, - "fill": "dark-orange", - "mode": "scheme", - "reverse": false, - "scale": "exponential", - "scheme": "Spectral", - "steps": 128 - }, - "exemplars": { - "color": "rgba(255,0,255,0.7)" - }, - "filterValues": { - "le": 1e-9 - }, - "legend": { - "show": true - }, - "rowsFrame": { - "layout": "auto" - }, - "tooltip": { - "mode": "single", - "showColorScale": false, - "yHistogram": false - }, - "yAxis": { - "axisPlacement": "left", - "reverse": false, - "unit": "s", - "decimals": 4 - } - }, - "pluginVersion": "11.2.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "rate(bgp_daemon_hostfile_sync_duration_seconds_bucket[5m])", - "format": "heatmap", - "refId": "A", - "legendFormat": "{{le}}" - } - ], - "title": "Host File Sync Duration Distribution", - "type": "heatmap" - }, - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisBorderShow": false, - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 50, - "gradientMode": "none", - "hideFrom": { - "tooltip": false, - "viz": false, - "legend": false - }, - "insertNulls": false, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - } - ] - }, - "unit": "ops" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 14 - }, - "id": 6, - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "11.2.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "expr": "rate(bgp_daemon_peer_sync_total[5m])", - "refId": "A", - "legendFormat": "{{event_type}}" - } - ], - "title": "Sync Events by Type (PUT/DELETE)", - "type": "timeseries" - } - ], - "refresh": "30s", - "schemaVersion": 39, - "tags": [ - "bgp", - "daemon", - "mesh", - "monitoring" - ], - "templating": { - "list": [ - { - "current": { - "selected": true, - "text": "All", - "value": "$__all" - }, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "definition": "label_values(bgp_daemon_peers_discovered, instance)", - "hide": 0, - "includeAll": true, - "multi": true, - "name": "instance", - "options": [], - "query": { - "qryType": 1, - "query": "label_values(bgp_daemon_peers_discovered, instance)", - "refId": "PrometheusVariableQueryEditor-VariableQuery" - }, - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "type": "query" - } - ] - }, - "time": { - "from": "now-1h", - "to": "now" - }, - "timepicker": {}, - "timezone": "browser", - "title": "BGP Daemon Overview", - "uid": "bgp-daemon-overview", - "version": 1, - "weekStart": "" -} diff --git a/configs/grafana/provisioning/dashboards/dashboards.yml b/configs/grafana/provisioning/dashboards/dashboards.yml deleted file mode 100644 index 58d8180..0000000 --- a/configs/grafana/provisioning/dashboards/dashboards.yml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: 1 - -providers: - - name: 'BGP Daemon Dashboards' - orgId: 1 - folder: 'BGP Monitoring' - type: file - disableDeletion: false - updateIntervalSeconds: 30 - allowUiUpdates: true - options: - path: /etc/grafana/dashboards - foldersFromFilesStructure: false diff --git a/configs/grafana/provisioning/datasources/prometheus.yml b/configs/grafana/provisioning/datasources/prometheus.yml deleted file mode 100644 index fa70169..0000000 --- a/configs/grafana/provisioning/datasources/prometheus.yml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: 1 - -datasources: - - name: Prometheus - type: prometheus - access: proxy - uid: prometheus - url: http://localhost:9090 - isDefault: true - editable: false - jsonData: - timeInterval: 15s - queryTimeout: 60s - httpMethod: POST diff --git a/configs/prometheus/prometheus.yml b/configs/prometheus/prometheus.yml deleted file mode 100644 index c2d84c5..0000000 --- a/configs/prometheus/prometheus.yml +++ /dev/null @@ -1,84 +0,0 @@ -# Prometheus Configuration -# Global settings and scrape configs for BGP network monitoring - -global: - scrape_interval: 15s - evaluation_interval: 15s - external_labels: - cluster: 'bgp-local-dev' - environment: 'development' - -# Scrape configurations -scrape_configs: - # Prometheus self-monitoring - - job_name: 'prometheus' - static_configs: - - targets: ['localhost:9090'] - labels: - service: 'prometheus' - - # BIRD BGP daemon metrics (via bird_exporter if available) - - job_name: 'bird' - static_configs: - - targets: - - 'bird1:9324' - - 'bird2:9324' - - 'bird3:9324' - - 'bird4:9324' - - 'bird5:9324' - labels: - service: 'bird' - protocol: 'bgp' - - # TINC mesh metrics (custom exporter or node_exporter) - - job_name: 'tinc' - static_configs: - - targets: - - 'tinc1:9100' - - 'tinc2:9100' - - 'tinc3:9100' - - 'tinc4:9100' - - 'tinc5:9100' - labels: - service: 'tinc' - protocol: 'vpn' - - # etcd cluster metrics - - job_name: 'etcd' - static_configs: - - targets: - - 'etcd1:2379' - - 'etcd2:2379' - - 'etcd3:2379' - - 'etcd4:2379' - - 'etcd5:2379' - labels: - service: 'etcd' - protocol: 'kvstore' - metrics_path: '/metrics' - - # BGP daemon (Go) metrics - # Note: daemons share network namespace with tinc containers - - job_name: 'bgp-daemon' - scrape_interval: 15s - static_configs: - - targets: - - 'tinc1:2112' - - 'tinc2:2112' - - 'tinc3:2112' - - 'tinc4:2112' - - 'tinc5:2112' - labels: - service: 'bgp-daemon' - component: 'automation' - metrics_path: '/metrics' - -# Alerting configuration (placeholder for Sprint 2) -# alerting: -# alertmanagers: -# - static_configs: -# - targets: ['alertmanager:9093'] - -# Rule files (placeholder for Sprint 2) -# rule_files: -# - '/etc/prometheus/rules/*.yml' diff --git a/configs/tinc/tinc-down.j2 b/configs/tinc/tinc-down.j2 deleted file mode 100644 index cb57266..0000000 --- a/configs/tinc/tinc-down.j2 +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -# TINC interface down script -# Executed when TINC interface is brought down - -# Remove from etcd -etcdctl --endpoints=http://etcd1:2379 del /peers/{{ tinc_name }} || true - -# Bring interface down -ip link set $INTERFACE down - -echo "TINC interface $INTERFACE brought down and removed from etcd" diff --git a/configs/tinc/tinc-up.j2 b/configs/tinc/tinc-up.j2 deleted file mode 100644 index 06a383d..0000000 --- a/configs/tinc/tinc-up.j2 +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/sh -# TINC interface up script -# Executed when TINC interface is brought up - -# Set interface up with reduced MTU (TINC overhead) -ip link set $INTERFACE up mtu 1400 - -# Configure IPv4 address -ip addr add 10.0.0.{{ node_id }}/24 dev $INTERFACE - -# Configure IPv6 address -ip -6 addr add 2001:db8::{{ node_id }}/64 dev $INTERFACE - -# Propagate to etcd for peer discovery -# Read public key and store complete peer info -if [ -f "/var/run/tinc/bgpmesh/rsa_key.pub" ]; then - # Read public key and escape for JSON (replace newlines with \n) - PUBLIC_KEY=$(cat /var/run/tinc/bgpmesh/rsa_key.pub | sed ':a;N;$!ba;s/\n/\\n/g' | sed 's/"/\\"/g') - - # Store complete peer info in etcd - # Use hostname (not tinc_name) for endpoint - must be DNS-resolvable - etcdctl --endpoints=http://etcd1:2379 put /peers/{{ tinc_name }} \ - "{\"ip\":\"10.0.0.{{ node_id }}\",\"endpoint\":\"{{ hostname }}:655\",\"key\":\"$PUBLIC_KEY\"}" || true -else - # Fallback: store minimal info if key not available yet - # Use hostname (not tinc_name) for endpoint - must be DNS-resolvable - etcdctl --endpoints=http://etcd1:2379 put /peers/{{ tinc_name }} \ - "{\"ip\":\"10.0.0.{{ node_id }}\",\"endpoint\":\"{{ hostname }}:655\"}" || true -fi - -echo "TINC interface $INTERFACE configured: 10.0.0.{{ node_id }}/24, 2001:db8::{{ node_id }}/64" diff --git a/configs/tinc/tinc.conf.j2 b/configs/tinc/tinc.conf.j2 deleted file mode 100644 index c7f4ce8..0000000 --- a/configs/tinc/tinc.conf.j2 +++ /dev/null @@ -1,15 +0,0 @@ -# TINC 1.0 Configuration -# Generated from template - -Name = {{ tinc_name }} -Mode = switch -Cipher = aes-256-cbc -Digest = sha256 -Port = {{ tinc_port }} -Interface = tinc0 - -# Compression (optional, can add overhead) -# Compression = 9 - -# Forwarding -# DeviceType = tun diff --git a/daemon-go/Makefile b/daemon-go/Makefile deleted file mode 100644 index 22963d4..0000000 --- a/daemon-go/Makefile +++ /dev/null @@ -1,90 +0,0 @@ -.PHONY: test test-coverage test-unit test-race test-integration build clean help -.PHONY: vet fmt lint deps coverage-html install build-race ci-test -.PHONY: deps-tidy deps-update watch - -# Variables -GO := go -GOFLAGS := -v -BINARY := bgp-daemon -BUILD_DIR := bin -COVERAGE_FILE := coverage.out - -# Build targets -build: ## Build the daemon binary - @mkdir -p $(BUILD_DIR) - $(GO) build $(GOFLAGS) -o $(BUILD_DIR)/$(BINARY) ./cmd/bgp-daemon - -build-race: ## Build with race detector - @mkdir -p $(BUILD_DIR) - $(GO) build $(GOFLAGS) -race -o $(BUILD_DIR)/$(BINARY)-race ./cmd/bgp-daemon - -install: build ## Install binary to $GOPATH/bin - $(GO) install ./cmd/bgp-daemon - -# Test targets -test: ## Run all tests - $(GO) test $(GOFLAGS) ./... - -test-unit: ## Run unit tests only (exclude integration) - $(GO) test $(GOFLAGS) -short ./... - -test-race: ## Run tests with race detector - $(GO) test $(GOFLAGS) -race ./... - -test-coverage: ## Run tests with coverage report - $(GO) test $(GOFLAGS) -coverprofile=$(COVERAGE_FILE) -covermode=atomic ./... - @echo "Coverage: $$($(GO) tool cover -func=$(COVERAGE_FILE) | grep total | awk '{print $$3}')" - -coverage-html: test-coverage ## Generate HTML coverage report - $(GO) tool cover -html=$(COVERAGE_FILE) -o coverage.html - @echo "Coverage report: coverage.html" - -test-integration: ## Run integration tests (requires Docker) - $(GO) test $(GOFLAGS) -tags=integration ./... - -# Code quality targets -vet: ## Run go vet - $(GO) vet ./... - -fmt: ## Format code with gofmt - $(GO) fmt ./... - -lint: ## Run golangci-lint (requires golangci-lint) - @if command -v golangci-lint >/dev/null 2>&1; then \ - golangci-lint run; \ - else \ - echo "golangci-lint not installed, skipping"; \ - fi - -# Dependency management -deps: ## Download dependencies - $(GO) mod download - $(GO) mod verify - -deps-tidy: ## Tidy dependencies - $(GO) mod tidy - -deps-update: ## Update dependencies - $(GO) get -u ./... - $(GO) mod tidy - -# Cleanup -clean: ## Clean build artifacts and coverage files - rm -rf $(BUILD_DIR) - rm -f $(COVERAGE_FILE) coverage.html - -# Development helpers -watch: ## Watch for changes and rebuild (requires entr) - @if command -v entr >/dev/null 2>&1; then \ - find . -name "*.go" | entr -r make build; \ - else \ - echo "entr not installed, install with: apt-get install entr"; \ - fi - -# CI targets (used by GitHub Actions) -ci-test: vet test-race test-coverage ## Run CI test suite - -help: ## Show this help - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' - -.DEFAULT_GOAL := help diff --git a/daemon-go/README.md b/daemon-go/README.md deleted file mode 100644 index 5e8423e..0000000 --- a/daemon-go/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# BGP Propagation Daemon - -Custom Go daemon for peer discovery, key distribution, and config synchronization in the BGP overlay network. - -## Features - -**Sprint 1 (Current):** -- etcd integration (watch `/peers/` for changes) -- mDNS peer discovery skeleton (over TINC interface) -- Basic logging and signal handling - -**Sprint 2 (Planned):** -- Full mDNS service discovery and advertisement -- Automatic TINC key distribution -- Config sync (bird.conf, tinc.conf) -- Health monitoring and metrics - -**Sprint 3+:** -- Chaos testing support -- Advanced metrics (Prometheus exporter) -- Automated failover logic - -## Build - -```bash -# Get dependencies -go mod download - -# Build binary -go build -o bgp-daemon ./cmd/bgp-daemon - -# Build with optimizations -go build -ldflags="-s -w" -o bgp-daemon ./cmd/bgp-daemon -``` - -## Run - -```bash -# Default (etcd on localhost:2379, interface tinc0) -./bgp-daemon - -# Custom etcd endpoint -./bgp-daemon -etcd etcd1:2379 - -# Custom TINC interface -./bgp-daemon -iface tun0 - -# Verbose logging -./bgp-daemon -v - -# All options -./bgp-daemon -etcd etcd1:2379,etcd2:2379 -iface tinc0 -v -``` - -## Flags - -- `-etcd`: etcd endpoints (comma-separated), default: `localhost:2379` -- `-iface`: TINC interface name, default: `tinc0` -- `-v`: Enable verbose logging - -## Development - -```bash -# Run tests -go test ./... - -# Run with race detector -go test -race ./... - -# Format code -go fmt ./... - -# Lint (requires golangci-lint) -golangci-lint run - -# View coverage -go test -cover ./... -go test -coverprofile=coverage.out ./... -go tool cover -html=coverage.out -``` - -## Docker Integration - -In Sprint 1, the daemon runs on the host (not containerized) and connects to Docker containers: - -```bash -# Run daemon connecting to Docker's etcd -./bgp-daemon -etcd localhost:2379 -``` - -## Architecture - -``` -┌─────────────┐ -│ Daemon │ -├─────────────┤ -│ Discovery │ ← mDNS over TINC -│ (mdns.go) │ -├─────────────┤ -│ Sync Logic │ ← etcd watch -│ (main.go) │ -├─────────────┤ -│ Types │ ← Peer struct -│ (types.go) │ -└─────────────┘ - ↓ - ┌───┴───┐ - ↓ ↓ -etcd TINC -cluster mesh -``` - -## Sprint 1 Limitations - -- mDNS discovery returns empty list (no services advertising yet) -- Key distribution not implemented (manual in Sprint 1) -- Config sync not implemented (using Docker volumes) -- Health monitoring basic (etcd watch only) - -These will be implemented in Sprint 2. - -## Dependencies - -- `github.com/hashicorp/mdns v1.0.5` - mDNS service discovery -- `go.etcd.io/etcd/client/v3 v3.5.14` - etcd client library - -## License - -TBD diff --git a/daemon-go/cmd/bgp-daemon/main.go b/daemon-go/cmd/bgp-daemon/main.go deleted file mode 100644 index 09bf895..0000000 --- a/daemon-go/cmd/bgp-daemon/main.go +++ /dev/null @@ -1,505 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "log" - "net" - "net/http" - "os" - "os/signal" - "strings" - "syscall" - "time" - - "github.com/pablomonte/bgp-daemon/pkg/discovery" - "github.com/pablomonte/bgp-daemon/pkg/metrics" - "github.com/pablomonte/bgp-daemon/pkg/tinc" - "github.com/pablomonte/bgp-daemon/pkg/types" - "github.com/prometheus/client_golang/prometheus/promhttp" - clientv3 "go.etcd.io/etcd/client/v3" -) - -var ( - etcdEndpoints = flag.String("etcd", "localhost:2379", "etcd endpoints (comma-separated)") - iface = flag.String("iface", "tinc0", "TINC interface name") - nodeName = flag.String("node", "node1", "Node name for mDNS advertisement") - tincNetName = flag.String("tinc-net", "bgpmesh", "TINC network name") - metricsAddr = flag.String("metrics-addr", ":2112", "Metrics HTTP server address") - verbose = flag.Bool("v", false, "verbose logging") -) - -func main() { - flag.Parse() - - log.SetFlags(log.LstdFlags | log.Lshortfile) - - log.Println("============================================") - log.Println("BGP Propagation Daemon") - log.Println("============================================") - log.Printf("Node name: %s", *nodeName) - log.Printf("TINC network: %s", *tincNetName) - log.Printf("TINC interface: %s", *iface) - log.Printf("etcd endpoints: %s", *etcdEndpoints) - log.Printf("Metrics address: %s", *metricsAddr) - log.Printf("Verbose: %v", *verbose) - log.Println() - - // Start Prometheus metrics HTTP server - log.Println("Starting metrics HTTP server...") - go func() { - http.Handle("/metrics", promhttp.Handler()) - if err := http.ListenAndServe(*metricsAddr, nil); err != nil { - log.Printf("⚠ Metrics server error: %v", err) - } - }() - log.Printf("✓ Metrics available at http://localhost%s/metrics", *metricsAddr) - log.Println() - - // Setup signal handling - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - go func() { - sig := <-sigChan - log.Printf("Received signal: %v", sig) - log.Println("Shutting down gracefully...") - cancel() - }() - - // Connect to etcd - log.Println("Connecting to etcd...") - cli, err := clientv3.New(clientv3.Config{ - Endpoints: []string{*etcdEndpoints}, - DialTimeout: 5 * time.Second, - }) - if err != nil { - log.Fatalf("Failed to connect to etcd: %v", err) - } - defer cli.Close() - - log.Println("✓ Connected to etcd") - - // Initialize TINC manager - log.Println() - log.Println("Initializing TINC manager...") - tincManager := tinc.NewManager(*tincNetName) - log.Println("✓ TINC manager initialized") - - // Get local public key for mDNS advertisement - localKey, err := tincManager.GetPublicKey(*nodeName) - keyFingerprint := "" - if err != nil { - log.Printf("⚠ Failed to read local key: %v", err) - log.Println(" Continuing without mDNS advertisement...") - keyFingerprint = "unknown" - } else { - log.Printf("✓ Read local public key (%d bytes)", len(localKey)) - // Use first 20 chars as fingerprint - if len(localKey) > 20 { - keyFingerprint = localKey[:20] - } else { - keyFingerprint = localKey - } - } - - // Store own key in etcd at /peers/ - nodeIP := os.Getenv("NODE_IP") - tincEndpoint := os.Getenv("TINC_ENDPOINT") - if localKey != "" && nodeIP != "" && tincEndpoint != "" { - log.Println() - log.Println("Storing own key in etcd...") - - peerData := types.Peer{ - IP: net.ParseIP(nodeIP), - Key: localKey, - Endpoint: tincEndpoint, - } - - if peerData.IsValid() { - peerJSON, err := json.Marshal(peerData) - if err == nil { - peerKey := "/peers/" + *nodeName - // Add 5-second timeout for Put operation - putCtx, putCancel := context.WithTimeout(ctx, 5*time.Second) - defer putCancel() - if _, err := cli.Put(putCtx, peerKey, string(peerJSON)); err != nil { - log.Printf("⚠ Failed to store own key in etcd: %v", err) - } else { - log.Printf("✓ Stored own key in etcd at %s", peerKey) - } - } else { - log.Printf("⚠ Failed to marshal peer JSON: %v", err) - } - } else { - log.Printf("⚠ Invalid peer data, skipping etcd storage") - } - } else { - log.Println() - log.Println("⚠ Skipping etcd key storage (missing NODE_IP or TINC_ENDPOINT env vars)") - } - - // Start mDNS service advertisement - log.Println() - log.Println("Starting mDNS service advertisement...") - mdnsServer, err := discovery.AdvertiseService(*nodeName, 655, keyFingerprint) - if err != nil { - log.Printf("⚠ mDNS advertisement failed: %v", err) - } else { - defer mdnsServer.Shutdown() - log.Printf("✓ Advertising as '%s._bgp-node._tcp.local'", *nodeName) - } - - // Start continuous mDNS monitoring in background - log.Println() - log.Println("Starting mDNS peer monitoring...") - go discovery.MonitorPeers(ctx, *iface, 30*time.Second, func(peers []types.Peer) { - log.Printf("📡 mDNS: Discovered %d peers", len(peers)) - metrics.PeersDiscovered.Set(float64(len(peers))) - if *verbose { - for i, peer := range peers { - log.Printf(" [%d] %v", i+1, peer) - } - } - // TODO: Optionally sync discovered peers to etcd - }) - log.Println("✓ mDNS monitoring started (30s interval)") - - // Initial peer discovery - log.Println() - log.Println("Performing initial mDNS discovery...") - peers, err := discovery.LookupPeers(*iface) - if err != nil { - log.Printf("⚠ Initial mDNS lookup failed: %v", err) - } else { - log.Printf("✓ Discovered %d peers initially", len(peers)) - if *verbose { - for i, peer := range peers { - log.Printf(" [%d] %v", i+1, peer) - } - } - } - - // Perform initial peer sync from etcd - log.Println() - log.Println("Syncing TINC keys from etcd...") - - // Heurística de "ventana de calma" para peer discovery - // Espera hasta que no haya nuevos peers registrándose (convergencia natural) - // con timeout de seguridad para no esperar indefinidamente - calmWindow := 2 * time.Second // Tiempo sin cambios = todos registrados - checkInterval := 500 * time.Millisecond - maxWaitTime := 10 * time.Second - - startTime := time.Now() - lastPeerCount := 0 - calmDuration := time.Duration(0) - - var resp *clientv3.GetResponse - - // Loop de discovery con heurística adaptativa - for { - // Add 3-second timeout for each Get attempt - getCtx, getCancel := context.WithTimeout(ctx, 3*time.Second) - resp, err = cli.Get(getCtx, "/peers/", clientv3.WithPrefix()) - getCancel() - if err != nil { - log.Printf("⚠ Failed to fetch peers: %v", err) - time.Sleep(checkInterval) - continue - } - - currentPeerCount := len(resp.Kvs) - - // Nuevos peers detectados - resetear ventana de calma - if currentPeerCount > lastPeerCount { - if lastPeerCount == 0 { - log.Printf("⏳ Discovered %d peers, waiting for cluster to stabilize...", currentPeerCount) - } else { - log.Printf("⏳ Discovered %d peers (was %d), waiting for more...", currentPeerCount, lastPeerCount) - } - lastPeerCount = currentPeerCount - calmDuration = 0 - time.Sleep(checkInterval) - continue - } - - // No hay cambios - incrementar tiempo de calma - calmDuration += checkInterval - - // Condición 1: Ventana de calma alcanzada (estable) - if calmDuration >= calmWindow { - log.Printf("✓ Peer discovery stable (%d peers found)", currentPeerCount) - break - } - - // Condición 2: Timeout máximo de seguridad - if time.Since(startTime) >= maxWaitTime { - log.Printf("⚠ Discovery timeout reached, proceeding with %d peers", currentPeerCount) - break - } - - time.Sleep(checkInterval) - } - - // Procesar todos los peers descubiertos - if err == nil { - peerNames := make([]string, 0) - syncedCount := 0 - - for _, kv := range resp.Kvs { - var peer types.Peer - if err := json.Unmarshal(kv.Value, &peer); err != nil { - log.Printf("⚠ Failed to parse peer JSON for %s: %v", string(kv.Key), err) - continue - } - - if !peer.IsValid() { - log.Printf("⚠ Invalid peer data for %s, skipping", string(kv.Key)) - continue - } - - peerNodeName := extractNodeNameFromKey(string(kv.Key)) - - // Skip syncing own host file - if peerNodeName == *nodeName { - if *verbose { - log.Printf(" Skipping own node: %s", peerNodeName) - } - continue - } - - // Sync host file - if err := tincManager.SyncHostFile(peerNodeName, peer); err != nil { - log.Printf("⚠ Failed to sync %s: %v", peerNodeName, err) - } else { - peerNames = append(peerNames, peerNodeName) - syncedCount++ - if *verbose { - log.Printf(" ✓ Synced host file for %s", peerNodeName) - } - } - } - - log.Printf("✓ Synced %d peer host files", syncedCount) - - // Reconcile TINC connections (file-based for TINC 1.0) - // Updates tinc.conf and reloads daemon - if len(peerNames) > 0 { - log.Println() - log.Println("Reconciling TINC connections...") - added, removed, err := tincManager.ReconcileConnections(peerNames) - if err != nil { - log.Printf("⚠ Reconciliation failed: %v", err) - } else { - log.Printf("✓ Connections reconciled (added: %d, removed: %d)", added, removed) - - // Update metrics - if added > 0 { - metrics.TincConnectionOperations.WithLabelValues("add", "success").Add(float64(added)) - } - if removed > 0 { - metrics.TincConnectionOperations.WithLabelValues("remove", "success").Add(float64(removed)) - } - - // Display current topology - if *verbose { - currentConns, _ := tincManager.GetCurrentConnections() - log.Printf("📊 Current connections: %v", currentConns) - metrics.TincConnectionsActive.Set(float64(len(currentConns))) - } - } - } - } - - // Watch etcd for peer changes - log.Println() - log.Println("Watching /peers/ in etcd for changes...") - watchChan := cli.Watch(ctx, "/peers/", clientv3.WithPrefix()) - - log.Println("✓ Daemon running (Ctrl+C to stop)") - log.Println("============================================") - log.Println() - - // Main event loop - for { - select { - case <-ctx.Done(): - log.Println("Context cancelled, exiting...") - return - - case watchResp := <-watchChan: - if watchResp.Err() != nil { - log.Printf("Watch error: %v", watchResp.Err()) - metrics.EtcdWatchErrors.Inc() - continue - } - - for _, event := range watchResp.Events { - key := string(event.Kv.Key) - - switch event.Type { - case clientv3.EventTypePut: - log.Printf("📥 etcd PUT: %s", key) - - // Parse peer from JSON - var peer types.Peer - if err := json.Unmarshal(event.Kv.Value, &peer); err != nil { - log.Printf("⚠ Failed to parse peer JSON: %v", err) - metrics.PeerSyncTotal.WithLabelValues("error", "PUT").Inc() - continue - } - - if !peer.IsValid() { - log.Printf("⚠ Invalid peer data, skipping") - metrics.PeerSyncTotal.WithLabelValues("error", "PUT").Inc() - continue - } - - peerNodeName := extractNodeNameFromKey(key) - - // Skip own node - if peerNodeName == *nodeName { - if *verbose { - log.Printf(" Skipping own node update") - } - continue - } - - if *verbose { - log.Printf(" Peer: %v", peer) - } - - // Step 1: Sync host file (persistent) - syncStart := time.Now() - if err := tincManager.SyncHostFile(peerNodeName, peer); err != nil { - log.Printf("❌ Failed to sync host file: %v", err) - metrics.PeerSyncTotal.WithLabelValues("error", "PUT").Inc() - continue - } - metrics.HostFileSyncDuration.Observe(time.Since(syncStart).Seconds()) - log.Printf("✓ Synced host file for %s", peerNodeName) - - // Step 2: Get all current peers from etcd - getCtx, getCancel := context.WithTimeout(ctx, 3*time.Second) - resp, err := cli.Get(getCtx, "/peers/", clientv3.WithPrefix()) - getCancel() - if err != nil { - log.Printf("⚠ Failed to fetch all peers: %v", err) - metrics.PeerSyncTotal.WithLabelValues("error", "PUT").Inc() - continue - } - - allPeerNames := make([]string, 0) - for _, kv := range resp.Kvs { - pn := extractNodeNameFromKey(string(kv.Key)) - if pn != *nodeName && pn != "" { - allPeerNames = append(allPeerNames, pn) - } - } - - // Step 3: Reconcile connections (TINC 1.0 file-based) - // Updates tinc.conf and reloads daemon - added, removed, err := tincManager.ReconcileConnections(allPeerNames) - if err != nil { - log.Printf("❌ Failed to reconcile connections: %v", err) - metrics.PeerSyncTotal.WithLabelValues("error", "PUT").Inc() - } else { - log.Printf("✓ Connections reconciled for %s (added: %d, removed: %d)", peerNodeName, added, removed) - metrics.PeerSyncTotal.WithLabelValues("success", "PUT").Inc() - - if added > 0 { - metrics.TincConnectionOperations.WithLabelValues("add", "success").Add(float64(added)) - } - if removed > 0 { - metrics.TincConnectionOperations.WithLabelValues("remove", "success").Add(float64(removed)) - } - - // Display current topology - if *verbose { - currentConns, _ := tincManager.GetCurrentConnections() - log.Printf("📊 Current connections: %v", currentConns) - metrics.TincConnectionsActive.Set(float64(len(currentConns))) - } - } - - case clientv3.EventTypeDelete: - log.Printf("🗑️ etcd DELETE: %s", key) - - // Extract node name from key (e.g., /peers/node2 -> node2) - deletedNodeName := extractNodeNameFromKey(key) - if deletedNodeName == "" { - log.Printf("⚠ Could not extract node name from key") - metrics.PeerSyncTotal.WithLabelValues("error", "DELETE").Inc() - continue - } - - log.Printf(" Removing peer: %s", deletedNodeName) - - // Step 1: Remove host file (persistent) - if err := tincManager.RemoveHostFile(deletedNodeName); err != nil { - log.Printf("⚠ Failed to remove host file: %v", err) - } else { - log.Printf("✓ Removed host file for %s", deletedNodeName) - } - - // Step 2: Get remaining peers from etcd - getCtx, getCancel := context.WithTimeout(ctx, 3*time.Second) - resp, err := cli.Get(getCtx, "/peers/", clientv3.WithPrefix()) - getCancel() - if err != nil { - log.Printf("⚠ Failed to fetch remaining peers: %v", err) - metrics.PeerSyncTotal.WithLabelValues("error", "DELETE").Inc() - continue - } - - remainingPeerNames := make([]string, 0) - for _, kv := range resp.Kvs { - pn := extractNodeNameFromKey(string(kv.Key)) - if pn != *nodeName && pn != "" { - remainingPeerNames = append(remainingPeerNames, pn) - } - } - - // Step 3: Reconcile connections (TINC 1.0 file-based) - // Updates tinc.conf and reloads daemon - added, removed, err := tincManager.ReconcileConnections(remainingPeerNames) - if err != nil { - log.Printf("❌ Failed to reconcile connections: %v", err) - metrics.PeerSyncTotal.WithLabelValues("error", "DELETE").Inc() - } else { - log.Printf("✓ Connections reconciled after removing %s (added: %d, removed: %d)", deletedNodeName, added, removed) - metrics.PeerSyncTotal.WithLabelValues("success", "DELETE").Inc() - - if removed > 0 { - metrics.TincConnectionOperations.WithLabelValues("remove", "success").Add(float64(removed)) - } - - // Display current topology - if *verbose { - currentConns, _ := tincManager.GetCurrentConnections() - log.Printf("📊 Current connections: %v", currentConns) - metrics.TincConnectionsActive.Set(float64(len(currentConns))) - } - } - } - } - } - } -} - -// extractNodeNameFromKey extracts the node name from an etcd key -// Examples: -// - "/peers/node2" -> "node2" -// - "/peers/tinc3" -> "tinc3" -func extractNodeNameFromKey(key string) string { - parts := strings.Split(key, "/") - if len(parts) > 0 { - return parts[len(parts)-1] - } - return "" -} diff --git a/daemon-go/go.mod b/daemon-go/go.mod deleted file mode 100644 index 6d5d80c..0000000 --- a/daemon-go/go.mod +++ /dev/null @@ -1,42 +0,0 @@ -module github.com/pablomonte/bgp-daemon - -go 1.23.0 - -toolchain go1.24.9 - -require ( - github.com/hashicorp/mdns v1.0.5 - github.com/prometheus/client_golang v1.23.2 - github.com/stretchr/testify v1.11.1 - go.etcd.io/etcd/client/v3 v3.5.14 -) - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/kylelemons/godebug v1.1.0 // indirect - github.com/miekg/dns v1.1.41 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - go.etcd.io/etcd/api/v3 v3.5.14 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.14 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect - google.golang.org/grpc v1.64.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/daemon-go/go.sum b/daemon-go/go.sum deleted file mode 100644 index 6f40bbe..0000000 --- a/daemon-go/go.sum +++ /dev/null @@ -1,117 +0,0 @@ -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= -github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/hashicorp/mdns v1.0.5 h1:1M5hW1cunYeoXOqHwEb/GBDDHAFo0Yqb/uz/beC6LbE= -github.com/hashicorp/mdns v1.0.5/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0= -go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU= -go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ= -go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI= -go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg= -go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw= -google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 h1:Zy9XzmMEflZ/MAaA7vNcoebnRAld7FsPW1EeBB7V0m8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/daemon-go/pkg/discovery/mdns.go b/daemon-go/pkg/discovery/mdns.go deleted file mode 100644 index 0557754..0000000 --- a/daemon-go/pkg/discovery/mdns.go +++ /dev/null @@ -1,156 +0,0 @@ -package discovery - -import ( - "context" - "fmt" - "time" - - "github.com/hashicorp/mdns" - "github.com/pablomonte/bgp-daemon/pkg/types" -) - -// LookupPeers discovers BGP peers via mDNS over the specified interface -// In Sprint 1, this is a skeleton implementation -// Sprint 2 will add full mDNS service discovery with TINC integration -func LookupPeers(iface string) ([]types.Peer, error) { - entries := make(chan *mdns.ServiceEntry, 10) - - // Query for BGP service on local network - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // Start mDNS query in goroutine - go func() { - defer close(entries) - - params := &mdns.QueryParam{ - Service: "_bgp-node._tcp", - Domain: "local", - Timeout: 5 * time.Second, - Entries: entries, - } - - // Run query - if err := mdns.Query(params); err != nil { - // Error is logged but not fatal for Sprint 1 - return - } - }() - - // Collect discovered peers - peers := []types.Peer{} - - for { - select { - case <-ctx.Done(): - // Timeout or cancelled - return peers, ctx.Err() - - case entry, ok := <-entries: - if !ok { - // Channel closed, query complete - return peers, nil - } - - if entry == nil { - continue - } - - // Convert mDNS entry to Peer struct - peer := types.Peer{ - IP: entry.AddrV4, - Endpoint: fmt.Sprintf("%s:%d", entry.AddrV4.String(), entry.Port), - } - - // Extract key from TXT records if available - if entry.InfoFields != nil && len(entry.InfoFields) > 0 { - peer.Key = entry.InfoFields[0] - } - - peers = append(peers, peer) - } - } -} - -// AdvertiseService broadcasts this node's BGP service via mDNS -// Advertises on _bgp-node._tcp.local with node info -func AdvertiseService(nodeName string, port int, keyFingerprint string) (*mdns.Server, error) { - // Create service info - info := []string{ - fmt.Sprintf("key=%s", keyFingerprint), - fmt.Sprintf("version=1.0"), - } - - // Define service - service, err := mdns.NewMDNSService( - nodeName, // Instance name (e.g., "node1") - "_bgp-node._tcp", // Service type - "", // Domain (empty = .local) - "", // Host name (empty = use hostname) - port, // Port - nil, // IPs (nil = use all interfaces) - info, // TXT records - ) - if err != nil { - return nil, fmt.Errorf("failed to create mDNS service: %w", err) - } - - // Start mDNS server - server, err := mdns.NewServer(&mdns.Config{Zone: service}) - if err != nil { - return nil, fmt.Errorf("failed to start mDNS server: %w", err) - } - - return server, nil -} - -// MonitorPeers continuously discovers peers and calls callback on changes -// Runs until context is cancelled -func MonitorPeers(ctx context.Context, iface string, interval time.Duration, callback func([]types.Peer)) error { - ticker := time.NewTicker(interval) - defer ticker.Stop() - - var lastPeers []types.Peer - - for { - select { - case <-ctx.Done(): - return ctx.Err() - - case <-ticker.C: - // Discover current peers - peers, err := LookupPeers(iface) - if err != nil { - // Log error but continue monitoring - continue - } - - // Check if peers changed - if !peersEqual(peers, lastPeers) { - callback(peers) - lastPeers = peers - } - } - } -} - -// peersEqual compares two peer lists for equality -func peersEqual(a, b []types.Peer) bool { - if len(a) != len(b) { - return false - } - - // Create map for O(n) comparison - aMap := make(map[string]bool) - for _, peer := range a { - aMap[peer.Endpoint] = true - } - - for _, peer := range b { - if !aMap[peer.Endpoint] { - return false - } - } - - return true -} diff --git a/daemon-go/pkg/discovery/mdns_test.go b/daemon-go/pkg/discovery/mdns_test.go deleted file mode 100644 index e00bdc4..0000000 --- a/daemon-go/pkg/discovery/mdns_test.go +++ /dev/null @@ -1,319 +0,0 @@ -package discovery - -import ( - "context" - "net" - "testing" - "time" - - "github.com/pablomonte/bgp-daemon/pkg/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPeersEqual(t *testing.T) { - tests := []struct { - name string - a []types.Peer - b []types.Peer - expected bool - }{ - { - name: "empty lists", - a: []types.Peer{}, - b: []types.Peer{}, - expected: true, - }, - { - name: "identical single peer", - a: []types.Peer{ - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - }, - b: []types.Peer{ - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - }, - expected: true, - }, - { - name: "identical multiple peers", - a: []types.Peer{ - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - {IP: net.ParseIP("10.0.0.3"), Endpoint: "10.0.0.3:655"}, - }, - b: []types.Peer{ - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - {IP: net.ParseIP("10.0.0.3"), Endpoint: "10.0.0.3:655"}, - }, - expected: true, - }, - { - name: "same peers different order", - a: []types.Peer{ - {IP: net.ParseIP("10.0.0.3"), Endpoint: "10.0.0.3:655"}, - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - }, - b: []types.Peer{ - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - {IP: net.ParseIP("10.0.0.3"), Endpoint: "10.0.0.3:655"}, - }, - expected: true, - }, - { - name: "different lengths", - a: []types.Peer{ - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - }, - b: []types.Peer{ - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - {IP: net.ParseIP("10.0.0.3"), Endpoint: "10.0.0.3:655"}, - }, - expected: false, - }, - { - name: "different endpoints", - a: []types.Peer{ - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - }, - b: []types.Peer{ - {IP: net.ParseIP("10.0.0.3"), Endpoint: "10.0.0.3:655"}, - }, - expected: false, - }, - { - name: "one empty one populated", - a: []types.Peer{ - {IP: net.ParseIP("10.0.0.2"), Endpoint: "10.0.0.2:655"}, - }, - b: []types.Peer{}, - expected: false, - }, - { - name: "nil vs empty", - a: nil, - b: []types.Peer{}, - expected: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := peersEqual(tt.a, tt.b) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestLookupPeers_Timeout(t *testing.T) { - if testing.Short() { - t.Skip("Skipping mDNS integration test in short mode") - } - - // Test that LookupPeers returns within timeout even with no peers - start := time.Now() - peers, err := LookupPeers("eth0") - elapsed := time.Since(start) - - // Should complete within ~5 seconds (with some buffer) - assert.Less(t, elapsed, 6*time.Second) - - // May return timeout error or nil depending on whether any responses came - if err != nil { - assert.ErrorIs(t, err, context.DeadlineExceeded) - } - - // Peers list should be valid (empty or populated) - assert.NotNil(t, peers) -} - -func TestLookupPeers_InvalidInterface(t *testing.T) { - if testing.Short() { - t.Skip("Skipping mDNS integration test in short mode") - } - - // LookupPeers should handle non-existent interface gracefully - // (hashicorp/mdns queries all interfaces when iface not supported) - peers, err := LookupPeers("nonexistent0") - - // Should not panic and return valid result - if err != nil { - assert.ErrorIs(t, err, context.DeadlineExceeded) - } - assert.NotNil(t, peers) -} - -func TestAdvertiseService(t *testing.T) { - if testing.Short() { - t.Skip("Skipping mDNS integration test in short mode") - } - - tests := []struct { - name string - nodeName string - port int - keyFingerprint string - wantErr bool - }{ - { - name: "valid service", - nodeName: "testnode", - port: 655, - keyFingerprint: "test-key-fingerprint", - wantErr: false, - }, - { - name: "valid service with empty key", - nodeName: "testnode2", - port: 655, - keyFingerprint: "", - wantErr: false, - }, - { - name: "high port number", - nodeName: "testnode3", - port: 65535, - keyFingerprint: "key", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - server, err := AdvertiseService(tt.nodeName, tt.port, tt.keyFingerprint) - - if tt.wantErr { - assert.Error(t, err) - assert.Nil(t, server) - return - } - - require.NoError(t, err) - require.NotNil(t, server) - - // Cleanup - server.Shutdown() - }) - } -} - -func TestAdvertiseService_InvalidPort(t *testing.T) { - if testing.Short() { - t.Skip("Skipping mDNS integration test in short mode") - } - - // Test with invalid port (0) - server, err := AdvertiseService("testnode", 0, "key") - - // Should fail with missing port error - assert.Error(t, err) - assert.Nil(t, server) - assert.Contains(t, err.Error(), "missing service port") -} - -func TestMonitorPeers_ContextCancellation(t *testing.T) { - if testing.Short() { - t.Skip("Skipping mDNS integration test in short mode") - } - - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - - callbackCalled := false - callback := func(peers []types.Peer) { - callbackCalled = true - } - - // Monitor with short interval - err := MonitorPeers(ctx, "eth0", 500*time.Millisecond, callback) - - // Should return context.DeadlineExceeded - assert.ErrorIs(t, err, context.DeadlineExceeded) - - // Callback may or may not have been called depending on timing - // We just verify it doesn't panic - _ = callbackCalled -} - -func TestMonitorPeers_ImmediateCancellation(t *testing.T) { - if testing.Short() { - t.Skip("Skipping mDNS integration test in short mode") - } - - ctx, cancel := context.WithCancel(context.Background()) - cancel() // Cancel immediately - - callbackCalled := false - callback := func(peers []types.Peer) { - callbackCalled = true - } - - start := time.Now() - err := MonitorPeers(ctx, "eth0", 1*time.Second, callback) - elapsed := time.Since(start) - - // Should return quickly - assert.Less(t, elapsed, 100*time.Millisecond) - assert.ErrorIs(t, err, context.Canceled) - assert.False(t, callbackCalled, "callback should not be called if context cancelled immediately") -} - -func TestMonitorPeers_CallbackOnChange(t *testing.T) { - if testing.Short() { - t.Skip("Skipping mDNS integration test in short mode") - } - - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) - defer cancel() - - callCount := 0 - var lastPeers []types.Peer - - callback := func(peers []types.Peer) { - callCount++ - lastPeers = peers - } - - // Monitor with short interval - // Note: In a real environment with mDNS traffic, callback might be called - // In test environment with no peers, callback is only called if peers change - err := MonitorPeers(ctx, "eth0", 500*time.Millisecond, callback) - - assert.ErrorIs(t, err, context.DeadlineExceeded) - - // If callback was called, verify peers structure - if callCount > 0 { - assert.NotNil(t, lastPeers) - } -} - -// TestAdvertiseAndDiscover_Integration has known data races in hashicorp/mdns library -// Disabled to pass CI race detector. The functions are tested individually above. -// -// func TestAdvertiseAndDiscover_Integration(t *testing.T) { -// if testing.Short() { -// t.Skip("Skipping mDNS integration test in short mode") -// } -// -// // Start advertising a service -// server, err := AdvertiseService("test-node", 655, "test-key") -// require.NoError(t, err) -// require.NotNil(t, server) -// defer server.Shutdown() -// -// // Give mDNS time to propagate -// time.Sleep(1 * time.Second) -// -// // Try to discover it -// peers, err := LookupPeers("eth0") -// -// // Should complete without error or with timeout -// if err != nil { -// assert.ErrorIs(t, err, context.DeadlineExceeded) -// } -// -// // Peers list should be valid -// assert.NotNil(t, peers) -// -// // Note: In containerized/isolated test environments, mDNS may not work -// // This test verifies the functions work together without panicking -// // but doesn't assert specific peer discovery due to network constraints -// } diff --git a/daemon-go/pkg/metrics/metrics.go b/daemon-go/pkg/metrics/metrics.go deleted file mode 100644 index b560148..0000000 --- a/daemon-go/pkg/metrics/metrics.go +++ /dev/null @@ -1,68 +0,0 @@ -package metrics - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" -) - -var ( - // PeerSyncTotal counts total peer sync operations - PeerSyncTotal = promauto.NewCounterVec( - prometheus.CounterOpts{ - Name: "bgp_daemon_peer_sync_total", - Help: "Total number of peer sync operations", - }, - []string{"status", "event_type"}, - ) - - // TincReloadDuration tracks TINC reload operation duration - TincReloadDuration = promauto.NewHistogram( - prometheus.HistogramOpts{ - Name: "bgp_daemon_tinc_reload_duration_seconds", - Help: "Duration of TINC daemon reload operations", - Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1}, - }, - ) - - // PeersDiscovered tracks number of peers discovered via mDNS - PeersDiscovered = promauto.NewGauge( - prometheus.GaugeOpts{ - Name: "bgp_daemon_peers_discovered", - Help: "Number of peers currently discovered via mDNS", - }, - ) - - // EtcdWatchErrors counts etcd watch errors - EtcdWatchErrors = promauto.NewCounter( - prometheus.CounterOpts{ - Name: "bgp_daemon_etcd_watch_errors_total", - Help: "Total number of etcd watch errors", - }, - ) - - // HostFileSyncDuration tracks host file sync duration - HostFileSyncDuration = promauto.NewHistogram( - prometheus.HistogramOpts{ - Name: "bgp_daemon_hostfile_sync_duration_seconds", - Help: "Duration of host file sync operations", - Buckets: []float64{.001, .005, .01, .025, .05, .1}, - }, - ) - - // TincConnectionsActive tracks number of active TINC connections - TincConnectionsActive = promauto.NewGauge( - prometheus.GaugeOpts{ - Name: "bgp_daemon_tinc_connections_active", - Help: "Number of active TINC connections maintained by daemon", - }, - ) - - // TincConnectionOperations tracks TINC connection add/remove operations - TincConnectionOperations = promauto.NewCounterVec( - prometheus.CounterOpts{ - Name: "bgp_daemon_tinc_connection_operations_total", - Help: "Total number of TINC connection operations (add/remove)", - }, - []string{"operation", "status"}, - ) -) diff --git a/daemon-go/pkg/metrics/metrics_test.go b/daemon-go/pkg/metrics/metrics_test.go deleted file mode 100644 index 1c025e8..0000000 --- a/daemon-go/pkg/metrics/metrics_test.go +++ /dev/null @@ -1,154 +0,0 @@ -package metrics - -import ( - "strings" - "testing" - - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/stretchr/testify/assert" -) - -func TestPeerSyncTotal_Registration(t *testing.T) { - // Verify metric is registered with correct name - assert.NotNil(t, PeerSyncTotal, "PeerSyncTotal should be initialized") -} - -func TestPeerSyncTotal_Labels(t *testing.T) { - // Reset counter before test - PeerSyncTotal.Reset() - - // Increment with success/PUT - PeerSyncTotal.WithLabelValues("success", "PUT").Inc() - PeerSyncTotal.WithLabelValues("success", "PUT").Inc() - PeerSyncTotal.WithLabelValues("error", "PUT").Inc() - PeerSyncTotal.WithLabelValues("success", "DELETE").Inc() - - // Verify counts - assert.Equal(t, float64(2), testutil.ToFloat64(PeerSyncTotal.WithLabelValues("success", "PUT"))) - assert.Equal(t, float64(1), testutil.ToFloat64(PeerSyncTotal.WithLabelValues("error", "PUT"))) - assert.Equal(t, float64(1), testutil.ToFloat64(PeerSyncTotal.WithLabelValues("success", "DELETE"))) - assert.Equal(t, float64(0), testutil.ToFloat64(PeerSyncTotal.WithLabelValues("error", "DELETE"))) -} - -func TestTincReloadDuration_Registration(t *testing.T) { - assert.NotNil(t, TincReloadDuration, "TincReloadDuration should be initialized") -} - -func TestTincReloadDuration_Buckets(t *testing.T) { - // Observe some values - TincReloadDuration.Observe(0.001) // 1ms - TincReloadDuration.Observe(0.010) // 10ms - TincReloadDuration.Observe(0.100) // 100ms - TincReloadDuration.Observe(0.500) // 500ms - - // For histograms, just verify it doesn't panic - // Actual bucket validation would require exporting and parsing the metric - assert.NotNil(t, TincReloadDuration) -} - -func TestPeersDiscovered_Registration(t *testing.T) { - assert.NotNil(t, PeersDiscovered, "PeersDiscovered should be initialized") -} - -func TestPeersDiscovered_SetAndGet(t *testing.T) { - // Set gauge values - PeersDiscovered.Set(3) - assert.Equal(t, float64(3), testutil.ToFloat64(PeersDiscovered)) - - PeersDiscovered.Set(5) - assert.Equal(t, float64(5), testutil.ToFloat64(PeersDiscovered)) - - PeersDiscovered.Set(0) - assert.Equal(t, float64(0), testutil.ToFloat64(PeersDiscovered)) -} - -func TestEtcdWatchErrors_Registration(t *testing.T) { - assert.NotNil(t, EtcdWatchErrors, "EtcdWatchErrors should be initialized") -} - -func TestEtcdWatchErrors_Increment(t *testing.T) { - // Get initial value - initial := testutil.ToFloat64(EtcdWatchErrors) - - // Increment - EtcdWatchErrors.Inc() - EtcdWatchErrors.Inc() - EtcdWatchErrors.Inc() - - // Verify increment - final := testutil.ToFloat64(EtcdWatchErrors) - assert.Equal(t, initial+3, final) -} - -func TestHostFileSyncDuration_Registration(t *testing.T) { - assert.NotNil(t, HostFileSyncDuration, "HostFileSyncDuration should be initialized") -} - -func TestHostFileSyncDuration_Observe(t *testing.T) { - // Observe typical sync durations - HostFileSyncDuration.Observe(0.0001) // 0.1ms - very fast - HostFileSyncDuration.Observe(0.005) // 5ms - normal - HostFileSyncDuration.Observe(0.050) // 50ms - slow - - // For histograms, just verify it doesn't panic - assert.NotNil(t, HostFileSyncDuration) -} - -func TestMetrics_PrometheusFormat(t *testing.T) { - // Reset all metrics - PeerSyncTotal.Reset() - PeersDiscovered.Set(2) - - // Increment some counters - PeerSyncTotal.WithLabelValues("success", "PUT").Inc() - - // Collect metrics - expected := ` - # HELP bgp_daemon_peer_sync_total Total number of peer sync operations - # TYPE bgp_daemon_peer_sync_total counter - bgp_daemon_peer_sync_total{event_type="PUT",status="success"} 1 - ` - - err := testutil.CollectAndCompare(PeerSyncTotal, strings.NewReader(expected)) - if err != nil { - // Just verify structure exists (exact values may vary) - t.Logf("Metric format check (non-fatal): %v", err) - } -} - -func TestMetrics_AllMetricsInitialized(t *testing.T) { - // Verify all metrics are initialized - metrics := []interface{}{ - PeerSyncTotal, - TincReloadDuration, - PeersDiscovered, - EtcdWatchErrors, - HostFileSyncDuration, - } - - for i, metric := range metrics { - assert.NotNil(t, metric, "Metric %d should be initialized", i) - } -} - -func TestMetrics_Concurrent(t *testing.T) { - // Test concurrent access to metrics (race detector will catch issues) - done := make(chan bool) - - for i := 0; i < 10; i++ { - go func() { - PeerSyncTotal.WithLabelValues("success", "PUT").Inc() - PeersDiscovered.Set(float64(i)) - TincReloadDuration.Observe(0.001) - done <- true - }() - } - - for i := 0; i < 10; i++ { - <-done - } - - // Verify some operations were recorded - count := testutil.ToFloat64(PeerSyncTotal.WithLabelValues("success", "PUT")) - assert.GreaterOrEqual(t, count, float64(10)) -} diff --git a/daemon-go/pkg/tinc/manager.go b/daemon-go/pkg/tinc/manager.go deleted file mode 100644 index b0a6739..0000000 --- a/daemon-go/pkg/tinc/manager.go +++ /dev/null @@ -1,337 +0,0 @@ -package tinc - -import ( - "encoding/base64" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/pablomonte/bgp-daemon/pkg/types" -) - -const ( - defaultTincDir = "/var/run/tinc" - defaultNetName = "bgpmesh" - hostsSubdir = "hosts" -) - -// Manager handles TINC configuration and operations -type Manager struct { - netName string - baseDir string - hostsDir string -} - -// NewManager creates a new TINC manager -func NewManager(netName string) *Manager { - baseDir := filepath.Join(defaultTincDir, netName) - hostsDir := filepath.Join(baseDir, hostsSubdir) - - return &Manager{ - netName: netName, - baseDir: baseDir, - hostsDir: hostsDir, - } -} - -// SyncHostFile creates or updates a host file for a peer -// nodeName is the TINC node name (e.g., "node2") used for the filename -// peer.Endpoint contains the DNS-resolvable hostname (e.g., "tinc2:655") for the Address field -func (m *Manager) SyncHostFile(nodeName string, peer types.Peer) error { - if nodeName == "" { - return fmt.Errorf("invalid peer: missing node name") - } - - hostFilePath := filepath.Join(m.hostsDir, nodeName) - - // Decode key if base64 encoded - keyData := peer.Key - if decoded, err := base64.StdEncoding.DecodeString(peer.Key); err == nil { - keyData = string(decoded) - } - - // Extract address from endpoint (remove port if present) - // This is the DNS-resolvable hostname (e.g., "tinc2") - address := peer.Endpoint - if idx := strings.Index(peer.Endpoint, ":"); idx != -1 { - address = peer.Endpoint[:idx] - } - - // Create host file content - // File is named with TINC node name (node2), but Address uses Docker hostname (tinc2) - // Subnet declaration is required for TINC switch mode to map IPs to nodes - content := fmt.Sprintf(`# Host configuration for %s -Address = %s -Port = 655 -Subnet = %s/32 - -%s -`, nodeName, address, peer.IP.String(), keyData) - - // Write host file - if err := os.WriteFile(hostFilePath, []byte(content), 0644); err != nil { - return fmt.Errorf("failed to write host file: %w", err) - } - - return nil -} - -// RemoveHostFile deletes a host file for a peer -func (m *Manager) RemoveHostFile(nodeName string) error { - hostFilePath := filepath.Join(m.hostsDir, nodeName) - - if err := os.Remove(hostFilePath); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove host file: %w", err) - } - - return nil -} - -// Reload triggers TINC daemon to reload configuration -// Uses SIGHUP signal (TINC 1.0 mechanism) with shared PID namespace -// Includes retry logic with exponential backoff for robustness -func (m *Manager) Reload() error { - // Retry with exponential backoff for transient errors - var lastErr error - for attempt := 1; attempt <= 3; attempt++ { - // Find tincd process PID (visible due to shared PID namespace) - pidCmd := exec.Command("pidof", "tincd") - output, err := pidCmd.Output() - - if err != nil { - lastErr = fmt.Errorf("attempt %d: tincd process not found: %w", attempt, err) - if attempt < 3 { - // Wait before retry - tincd might be starting - backoff := time.Duration(1<" lines - if strings.HasPrefix(trimmed, "ConnectTo") { - // Parse: "ConnectTo = node2" or "ConnectTo=node2" - parts := strings.SplitN(trimmed, "=", 2) - if len(parts) == 2 { - peerName := strings.TrimSpace(parts[1]) - if peerName != "" { - peers = append(peers, peerName) - } - } - } - } - - return peers, nil -} - -// GetDesiredConnections returns list of peers that SHOULD be connected (full mesh logic) -// Excludes own node name -func (m *Manager) GetDesiredConnections(allPeers []string, ownNodeName string) []string { - desired := make([]string, 0) - for _, peer := range allPeers { - if peer != ownNodeName && peer != "" { - desired = append(desired, peer) - } - } - return desired -} - -// ReconcileConnections implements full mesh for TINC 1.0 (file-based) -// Updates tinc.conf with desired peers and reloads daemon -// Returns (added, removed, error) -func (m *Manager) ReconcileConnections(desiredPeers []string) (int, int, error) { - // Get current connections from tinc.conf - current, err := m.GetCurrentConnections() - if err != nil { - return 0, 0, fmt.Errorf("failed to get current connections: %w", err) - } - - // Calculate diffs for metrics - currentSet := make(map[string]bool) - for _, peer := range current { - currentSet[peer] = true - } - - desiredSet := make(map[string]bool) - for _, peer := range desiredPeers { - desiredSet[peer] = true - } - - added := 0 - removed := 0 - - for _, peer := range desiredPeers { - if !currentSet[peer] { - added++ - } - } - - for _, peer := range current { - if !desiredSet[peer] { - removed++ - } - } - - // Update tinc.conf with full peer list - if err := m.UpdateConnectTo(desiredPeers); err != nil { - return 0, 0, fmt.Errorf("failed to update tinc.conf: %w", err) - } - - // Reload TINC daemon to apply changes (SIGHUP) - if err := m.Reload(); err != nil { - return added, removed, fmt.Errorf("failed to reload tincd: %w", err) - } - - return added, removed, nil -} - -// extractNodeName extracts the node name from a peer -// Tries to parse from IP or endpoint -func extractNodeName(peer types.Peer) string { - // If endpoint contains a hostname, extract it - endpoint := peer.Endpoint - if strings.Contains(endpoint, ":") { - parts := strings.Split(endpoint, ":") - if len(parts) > 0 { - return parts[0] - } - } - - // Fallback: Use IP to generate node name (e.g., 10.0.0.2 -> node2) - ip := peer.IP.String() - parts := strings.Split(ip, ".") - if len(parts) == 4 { - return fmt.Sprintf("node%s", parts[3]) - } - - return "" -} diff --git a/daemon-go/pkg/tinc/manager_test.go b/daemon-go/pkg/tinc/manager_test.go deleted file mode 100644 index 559cdb1..0000000 --- a/daemon-go/pkg/tinc/manager_test.go +++ /dev/null @@ -1,520 +0,0 @@ -package tinc - -import ( - "net" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/pablomonte/bgp-daemon/pkg/types" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewManager(t *testing.T) { - manager := NewManager("testnet") - - assert.NotNil(t, manager) - assert.Equal(t, "testnet", manager.netName) - assert.Equal(t, "/var/run/tinc/testnet", manager.baseDir) - assert.Equal(t, "/var/run/tinc/testnet/hosts", manager.hostsDir) -} - -func TestNewManager_DefaultNetName(t *testing.T) { - manager := NewManager(defaultNetName) - - assert.Equal(t, defaultNetName, manager.netName) - assert.Equal(t, "/var/run/tinc/bgpmesh", manager.baseDir) -} - -func TestSyncHostFile(t *testing.T) { - // Create temporary directory for test - tmpDir, err := os.MkdirTemp("", "tinc-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - // Create hosts subdirectory - hostsDir := filepath.Join(tmpDir, "hosts") - err = os.MkdirAll(hostsDir, 0755) - require.NoError(t, err) - - // Create manager with test directory - manager := &Manager{ - netName: "testnet", - baseDir: tmpDir, - hostsDir: hostsDir, - } - - tests := []struct { - name string - peer types.Peer - wantErr bool - }{ - { - name: "valid peer with hostname endpoint", - peer: types.Peer{ - IP: net.ParseIP("10.0.0.2"), - Endpoint: "node2:655", - Key: "-----BEGIN RSA PUBLIC KEY-----\ntest\n-----END RSA PUBLIC KEY-----", - }, - wantErr: false, - }, - { - name: "valid peer with IP endpoint", - peer: types.Peer{ - IP: net.ParseIP("10.0.0.3"), - Endpoint: "192.168.1.3:655", - Key: "test-key", - }, - wantErr: false, - }, - { - name: "peer with base64 encoded key", - peer: types.Peer{ - IP: net.ParseIP("10.0.0.4"), - Endpoint: "node4:655", - Key: "dGVzdC1rZXk=", // "test-key" in base64 - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Extract node name from peer (matches production logic) - nodeName := extractNodeName(tt.peer) - err := manager.SyncHostFile(nodeName, tt.peer) - - if tt.wantErr { - assert.Error(t, err) - return - } - - require.NoError(t, err) - - // Verify file was created - hostFile := filepath.Join(hostsDir, nodeName) - assert.FileExists(t, hostFile) - - // Verify file content - content, err := os.ReadFile(hostFile) - require.NoError(t, err) - - // Extract expected address from endpoint (hostname without port) - expectedAddr := tt.peer.Endpoint - if idx := strings.Index(tt.peer.Endpoint, ":"); idx != -1 { - expectedAddr = tt.peer.Endpoint[:idx] - } - - assert.Contains(t, string(content), expectedAddr) - assert.Contains(t, string(content), "Port = 655") - }) - } -} - -func TestSyncHostFile_InvalidPeer(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "tinc-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - hostsDir := filepath.Join(tmpDir, "hosts") - err = os.MkdirAll(hostsDir, 0755) - require.NoError(t, err) - - manager := &Manager{ - netName: "testnet", - baseDir: tmpDir, - hostsDir: hostsDir, - } - - // Peer with no valid node name extraction - peer := types.Peer{ - IP: net.ParseIP("::1"), // IPv6 won't work with current extractNodeName - Endpoint: "", - Key: "test-key", - } - - // Pass empty nodeName to test error handling - err = manager.SyncHostFile("", peer) - assert.Error(t, err) - assert.Contains(t, err.Error(), "missing node name") -} - -func TestRemoveHostFile(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "tinc-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - hostsDir := filepath.Join(tmpDir, "hosts") - err = os.MkdirAll(hostsDir, 0755) - require.NoError(t, err) - - manager := &Manager{ - netName: "testnet", - baseDir: tmpDir, - hostsDir: hostsDir, - } - - // Create a test file - testFile := filepath.Join(hostsDir, "node2") - err = os.WriteFile(testFile, []byte("test content"), 0644) - require.NoError(t, err) - - // Remove the file - err = manager.RemoveHostFile("node2") - assert.NoError(t, err) - assert.NoFileExists(t, testFile) -} - -func TestRemoveHostFile_NonExistent(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "tinc-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - hostsDir := filepath.Join(tmpDir, "hosts") - err = os.MkdirAll(hostsDir, 0755) - require.NoError(t, err) - - manager := &Manager{ - netName: "testnet", - baseDir: tmpDir, - hostsDir: hostsDir, - } - - // Try to remove non-existent file (should not error) - err = manager.RemoveHostFile("nonexistent") - assert.NoError(t, err) -} - -func TestGetPublicKey(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "tinc-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - hostsDir := filepath.Join(tmpDir, "hosts") - err = os.MkdirAll(hostsDir, 0755) - require.NoError(t, err) - - manager := &Manager{ - netName: "testnet", - baseDir: tmpDir, - hostsDir: hostsDir, - } - - tests := []struct { - name string - fileContent string - wantKey string - wantErr bool - }{ - { - name: "valid host file", - fileContent: `# Host configuration for node2 -Address = 10.0.0.2 -Port = 655 - ------BEGIN RSA PUBLIC KEY----- -test-public-key-data ------END RSA PUBLIC KEY-----`, - wantKey: "-----BEGIN RSA PUBLIC KEY-----\ntest-public-key-data\n-----END RSA PUBLIC KEY-----", - wantErr: false, - }, - { - name: "host file with extra blank lines", - fileContent: `Address = 10.0.0.3 - -my-key-data`, - wantKey: "my-key-data", - wantErr: false, - }, - { - name: "invalid format - no blank line", - fileContent: `Address = 10.0.0.4`, - wantKey: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create test file - testFile := filepath.Join(hostsDir, "testnode") - err := os.WriteFile(testFile, []byte(tt.fileContent), 0644) - require.NoError(t, err) - - key, err := manager.GetPublicKey("testnode") - - if tt.wantErr { - assert.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.wantKey, key) - }) - } -} - -func TestGetPublicKey_NonExistentFile(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "tinc-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - hostsDir := filepath.Join(tmpDir, "hosts") - err = os.MkdirAll(hostsDir, 0755) - require.NoError(t, err) - - manager := &Manager{ - netName: "testnet", - baseDir: tmpDir, - hostsDir: hostsDir, - } - - _, err = manager.GetPublicKey("nonexistent") - assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to read host file") -} - -func TestExtractNodeName(t *testing.T) { - tests := []struct { - name string - peer types.Peer - expected string - }{ - { - name: "hostname in endpoint", - peer: types.Peer{ - IP: net.ParseIP("10.0.0.2"), - Endpoint: "node2:655", - }, - expected: "node2", - }, - { - name: "FQDN in endpoint", - peer: types.Peer{ - IP: net.ParseIP("10.0.0.3"), - Endpoint: "node3.local:655", - }, - expected: "node3.local", - }, - { - name: "IP endpoint - returns IP from endpoint", - peer: types.Peer{ - IP: net.ParseIP("10.0.0.4"), - Endpoint: "192.168.1.4:655", - }, - expected: "192.168.1.4", - }, - { - name: "no port in endpoint - fallback to IP-based name", - peer: types.Peer{ - IP: net.ParseIP("10.0.0.4"), - Endpoint: "", - }, - expected: "node4", - }, - { - name: "endpoint without port", - peer: types.Peer{ - IP: net.ParseIP("10.0.0.5"), - Endpoint: "node5", - }, - expected: "node5", - }, - { - name: "IPv6 - returns empty", - peer: types.Peer{ - IP: net.ParseIP("::1"), - Endpoint: "[::1]:655", - }, - expected: "[", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := extractNodeName(tt.peer) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestUpdateConnectTo(t *testing.T) { - // Create temporary directory for test - tmpDir, err := os.MkdirTemp("", "tinc-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - // Create tinc.conf with initial content - confPath := filepath.Join(tmpDir, "tinc.conf") - initialConf := `Name = node1 -Mode = switch -Port = 655 -` - err = os.WriteFile(confPath, []byte(initialConf), 0644) - require.NoError(t, err) - - // Create manager with test directory - manager := &Manager{ - netName: "testnet", - baseDir: tmpDir, - hostsDir: filepath.Join(tmpDir, "hosts"), - } - - tests := []struct { - name string - peers []string - wantLines []string - }{ - { - name: "add single peer", - peers: []string{"node2"}, - wantLines: []string{ - "Name = node1", - "Mode = switch", - "Port = 655", - "ConnectTo = node2", - }, - }, - { - name: "add multiple peers", - peers: []string{"node2", "node3", "node4"}, - wantLines: []string{ - "Name = node1", - "Mode = switch", - "Port = 655", - "ConnectTo = node2", - "ConnectTo = node3", - "ConnectTo = node4", - }, - }, - { - name: "replace existing peers", - peers: []string{"node5"}, - wantLines: []string{ - "Name = node1", - "Mode = switch", - "Port = 655", - "ConnectTo = node5", - }, - }, - { - name: "empty peer list", - peers: []string{}, - wantLines: []string{ - "Name = node1", - "Mode = switch", - "Port = 655", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Update ConnectTo - err := manager.UpdateConnectTo(tt.peers) - require.NoError(t, err) - - // Read back the config - data, err := os.ReadFile(confPath) - require.NoError(t, err) - - // Verify each expected line is present - content := string(data) - for _, wantLine := range tt.wantLines { - assert.Contains(t, content, wantLine, "config should contain: %s", wantLine) - } - - // Verify no duplicate ConnectTo lines - lines := strings.Split(content, "\n") - connectToCount := 0 - for _, line := range lines { - if strings.HasPrefix(strings.TrimSpace(line), "ConnectTo") { - connectToCount++ - } - } - assert.Equal(t, len(tt.peers), connectToCount, "should have exactly %d ConnectTo lines", len(tt.peers)) - }) - } -} - -func TestUpdateConnectTo_NonExistentFile(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "tinc-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - manager := &Manager{ - netName: "testnet", - baseDir: tmpDir, - hostsDir: filepath.Join(tmpDir, "hosts"), - } - - // Try to update ConnectTo when tinc.conf doesn't exist - // Should return nil (graceful handling) instead of error during startup - err = manager.UpdateConnectTo([]string{"node2"}) - assert.NoError(t, err, "UpdateConnectTo should handle missing tinc.conf gracefully") -} - -func TestUpdateConnectTo_PreservesOtherLines(t *testing.T) { - tmpDir, err := os.MkdirTemp("", "tinc-test-*") - require.NoError(t, err) - defer os.RemoveAll(tmpDir) - - confPath := filepath.Join(tmpDir, "tinc.conf") - initialConf := `Name = node1 -Mode = switch -Port = 655 -ConnectTo = oldpeer1 -ConnectTo = oldpeer2 -Device = /dev/net/tun -AddressFamily = ipv4 -` - err = os.WriteFile(confPath, []byte(initialConf), 0644) - require.NoError(t, err) - - manager := &Manager{ - netName: "testnet", - baseDir: tmpDir, - hostsDir: filepath.Join(tmpDir, "hosts"), - } - - // Update with new peers - err = manager.UpdateConnectTo([]string{"newpeer1", "newpeer2"}) - require.NoError(t, err) - - // Read back - data, err := os.ReadFile(confPath) - require.NoError(t, err) - content := string(data) - - // Old ConnectTo lines should be removed - assert.NotContains(t, content, "ConnectTo = oldpeer1") - assert.NotContains(t, content, "ConnectTo = oldpeer2") - - // New ConnectTo lines should be present - assert.Contains(t, content, "ConnectTo = newpeer1") - assert.Contains(t, content, "ConnectTo = newpeer2") - - // Other config lines should be preserved - assert.Contains(t, content, "Name = node1") - assert.Contains(t, content, "Mode = switch") - assert.Contains(t, content, "Port = 655") - assert.Contains(t, content, "Device = /dev/net/tun") - assert.Contains(t, content, "AddressFamily = ipv4") -} - -func TestReload_Integration(t *testing.T) { - // Skip if not in integration mode or tincd not available - if testing.Short() { - t.Skip("Skipping integration test in short mode") - } - - manager := NewManager("testnet") - - // This will likely fail unless tincd is actually running - // We just test that the function doesn't panic - err := manager.Reload() - // We don't assert success/failure since it depends on tincd being available - _ = err -} diff --git a/daemon-go/pkg/types/types.go b/daemon-go/pkg/types/types.go deleted file mode 100644 index 2a807c9..0000000 --- a/daemon-go/pkg/types/types.go +++ /dev/null @@ -1,52 +0,0 @@ -package types - -import ( - "fmt" - "net" -) - -// Peer represents a discovered BGP peer in the TINC mesh -type Peer struct { - IP net.IP // IPv4 address on TINC mesh (e.g., 10.0.0.2) - Key string // RSA public key (base64 or fingerprint) - Endpoint string // External endpoint for TINC connection (IP:port) -} - -// String returns a human-readable representation of the peer -func (p Peer) String() string { - keyPreview := p.Key - if len(keyPreview) > 20 { - keyPreview = keyPreview[:20] + "..." - } - - return fmt.Sprintf("Peer{IP: %s, Endpoint: %s, Key: %s}", - p.IP.String(), p.Endpoint, keyPreview) -} - -// IsValid checks if the peer has all required fields -func (p Peer) IsValid() bool { - return p.IP != nil && p.Endpoint != "" -} - -// TODO Sprint 2: Add more peer metadata -// - Hostname/NodeName -// - BGP AS number -// - TINC subnet assignments -// - Health status (last seen, RTT) -// - Capabilities/features - -// TODO Sprint 2: Add config sync types -// type Config struct { -// BirdConf string -// TincConf string -// Version int -// } - -// TODO Sprint 2: Add health check types -// type HealthStatus struct { -// NodeName string -// LastSeen time.Time -// RTT time.Duration -// BGPStatus string // "Established", "Idle", etc. -// TINCStatus string // "Connected", "Disconnected" -// } diff --git a/daemon-go/pkg/types/types_test.go b/daemon-go/pkg/types/types_test.go deleted file mode 100644 index 49739e0..0000000 --- a/daemon-go/pkg/types/types_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package types - -import ( - "net" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPeer_String(t *testing.T) { - tests := []struct { - name string - peer Peer - expected string - }{ - { - name: "normal peer with short key", - peer: Peer{ - IP: net.ParseIP("10.0.0.2"), - Endpoint: "192.168.1.2:655", - Key: "short-key", - }, - expected: "Peer{IP: 10.0.0.2, Endpoint: 192.168.1.2:655, Key: short-key}", - }, - { - name: "peer with long key gets truncated", - peer: Peer{ - IP: net.ParseIP("10.0.0.3"), - Endpoint: "192.168.1.3:655", - Key: "this-is-a-very-long-key-that-should-be-truncated", - }, - expected: "Peer{IP: 10.0.0.3, Endpoint: 192.168.1.3:655, Key: this-is-a-very-long-...}", - }, - { - name: "peer with empty key", - peer: Peer{ - IP: net.ParseIP("10.0.0.4"), - Endpoint: "192.168.1.4:655", - Key: "", - }, - expected: "Peer{IP: 10.0.0.4, Endpoint: 192.168.1.4:655, Key: }", - }, - { - name: "peer with nil IP", - peer: Peer{ - IP: nil, - Endpoint: "192.168.1.5:655", - Key: "test-key", - }, - expected: "Peer{IP: , Endpoint: 192.168.1.5:655, Key: test-key}", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := tt.peer.String() - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestPeer_IsValid(t *testing.T) { - tests := []struct { - name string - peer Peer - expected bool - }{ - { - name: "valid peer with all fields", - peer: Peer{ - IP: net.ParseIP("10.0.0.2"), - Endpoint: "192.168.1.2:655", - Key: "some-key", - }, - expected: true, - }, - { - name: "valid peer without key", - peer: Peer{ - IP: net.ParseIP("10.0.0.3"), - Endpoint: "192.168.1.3:655", - Key: "", - }, - expected: true, - }, - { - name: "invalid peer with nil IP", - peer: Peer{ - IP: nil, - Endpoint: "192.168.1.4:655", - Key: "some-key", - }, - expected: false, - }, - { - name: "invalid peer with empty endpoint", - peer: Peer{ - IP: net.ParseIP("10.0.0.5"), - Endpoint: "", - Key: "some-key", - }, - expected: false, - }, - { - name: "invalid peer with both nil IP and empty endpoint", - peer: Peer{ - IP: nil, - Endpoint: "", - Key: "some-key", - }, - expected: false, - }, - { - name: "valid peer with IPv6", - peer: Peer{ - IP: net.ParseIP("2001:db8::1"), - Endpoint: "[2001:db8::2]:655", - Key: "ipv6-key", - }, - expected: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := tt.peer.IsValid() - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestPeer_EdgeCases(t *testing.T) { - t.Run("peer with exactly 20 char key not truncated", func(t *testing.T) { - peer := Peer{ - IP: net.ParseIP("10.0.0.1"), - Endpoint: "test:655", - Key: "12345678901234567890", // Exactly 20 chars - } - str := peer.String() - assert.Contains(t, str, "12345678901234567890") - assert.NotContains(t, str, "...") - }) - - t.Run("peer with 21 char key gets truncated", func(t *testing.T) { - peer := Peer{ - IP: net.ParseIP("10.0.0.1"), - Endpoint: "test:655", - Key: "123456789012345678901", // 21 chars - } - str := peer.String() - assert.Contains(t, str, "12345678901234567890...") - }) - - t.Run("valid peer with whitespace in endpoint", func(t *testing.T) { - peer := Peer{ - IP: net.ParseIP("10.0.0.1"), - Endpoint: " 192.168.1.1:655 ", - Key: "test", - } - // Endpoint is not empty, so it's technically valid - assert.True(t, peer.IsValid()) - }) - - t.Run("empty struct is invalid", func(t *testing.T) { - peer := Peer{} - assert.False(t, peer.IsValid()) - }) -} - -func TestPeer_Construction(t *testing.T) { - t.Run("create peer with ParseIP", func(t *testing.T) { - ip := net.ParseIP("10.0.0.100") - require.NotNil(t, ip) - - peer := Peer{ - IP: ip, - Endpoint: "external.example.com:655", - Key: "rsa-public-key", - } - - assert.True(t, peer.IsValid()) - assert.Equal(t, "10.0.0.100", peer.IP.String()) - assert.Contains(t, peer.String(), "10.0.0.100") - }) - - t.Run("create peer with invalid IP string", func(t *testing.T) { - ip := net.ParseIP("invalid-ip") - assert.Nil(t, ip) - - peer := Peer{ - IP: ip, - Endpoint: "test:655", - Key: "key", - } - - assert.False(t, peer.IsValid()) - }) -} diff --git a/deploy/laptop-border/Caddyfile b/deploy/laptop-border/Caddyfile new file mode 100644 index 0000000..fad0b44 --- /dev/null +++ b/deploy/laptop-border/Caddyfile @@ -0,0 +1,4 @@ +:443 { + tls /certs/server.crt /certs/server.key + reverse_proxy netmaker:8081 +} diff --git a/deploy/laptop-border/Dockerfile b/deploy/laptop-border/Dockerfile new file mode 100644 index 0000000..681e8ad --- /dev/null +++ b/deploy/laptop-border/Dockerfile @@ -0,0 +1,17 @@ +FROM debian:12-slim + +RUN apt-get update && apt-get install -y \ + bird2 \ + iproute2 \ + && rm -rf /var/lib/apt/lists/* + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 179 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s \ + CMD birdc show status || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] + diff --git a/deploy/laptop-border/SETUP.md b/deploy/laptop-border/SETUP.md new file mode 100644 index 0000000..369cd36 --- /dev/null +++ b/deploy/laptop-border/SETUP.md @@ -0,0 +1,143 @@ +# Border Router Setup (AS 65000) + +## Before deploying + +### 1. Configure IPs + +Edit `bird.conf` and replace: +- `172.30.0.100` → This laptop's physical IP +- `172.30.0.1` → RPi ISP's physical IP (BGP neighbor) + +### 2. Create `.env` file + +```bash +cat < .env +SERVER_HOST=172.30.0.100 +MASTER_KEY=$(openssl rand -base64 32) +ENROLLMENT_TOKEN= +EOF +``` + +**Note:** `ENROLLMENT_TOKEN` is set after creating the network in Netmaker (see step 5). + +### 3. Network requirements + +- Same LAN as RPi ISP +- Ports needed: + - 179/TCP (BGP) + - 443/TCP (Netmaker API via Caddy TLS proxy) + - 51821/UDP (WireGuard) + - 1883/TCP (MQTT) + +### 4. Install CA certificate on host + +The self-signed certificate must be trusted by the host for netclient to work: + +```bash +sudo cp certs/server.crt /usr/local/share/ca-certificates/netmaker.crt +sudo update-ca-certificates +``` + +## Deploy + +```bash +docker compose up -d +``` + +### 5. Create Netmaker network + +After deployment, create the mesh network via API: + +```bash +source .env + +# Create network +curl -sk -X POST "https://localhost/api/networks" \ + -H "Authorization: Bearer $MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "netid": "mesh", + "addressrange": "44.30.127.0/24" + }' + +# Create enrollment key +curl -sk -X POST "https://localhost/api/v1/enrollment-keys" \ + -H "Authorization: Bearer $MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "networks": ["mesh"], + "tags": ["mesh-node"], + "unlimited": true + }' +``` + +Save the `token` field from the enrollment key response. + +### 6. Enroll this node + +Update `.env` with the `ENROLLMENT_TOKEN` and restart netclient: + +```bash +# Add token to .env +echo "ENROLLMENT_TOKEN=" >> .env + +# Restart netclient +docker compose up -d --force-recreate netclient +``` + +### 7. Reload BIRD to detect netmaker interface + +After netclient creates the WireGuard interface, restart BIRD: + +```bash +docker restart bird-border +``` + +## Verify + +```bash +# Check BIRD/BGP +docker exec bird-border birdc show protocols +docker exec bird-border birdc show route +docker exec bird-border birdc "show route export isp" + +# Check Netmaker +docker logs netmaker +docker logs netclient +docker exec netclient wg show + +# Check mesh connectivity from netmaker IP +ping -I 44.30.127.1 -c 3 172.30.0.1 + +# Check API health +curl -sk https://localhost/api/server/health +``` + +## Architecture + +``` + ┌─────────────────────────────────────────────┐ + │ laptop-border (this) │ + │ │ + ISP (172.30.0.1) │ ┌─────────┐ ┌─────────┐ ┌──────────┐ │ + ◄────BGP:179────►│ │ BIRD │ │Netmaker │ │ Caddy │ │ + │ │ AS65000 │ │ Server │◄──│ :443 TLS │ │ + │ └────┬────┘ └────┬────┘ └──────────┘ │ + │ │ │ │ + │ │ ┌────┴────┐ │ + │ └────────►│Netclient│◄── WireGuard │ + │ │44.30.127.1 :51821 │ + │ └─────────┘ │ + └─────────────────────────────────────────────┘ + │ + │ WireGuard tunnel + ▼ + Other mesh nodes +``` + +## Security Note + +⚠️ Current setup uses self-signed certificates for testing. For production: +- Use Let's Encrypt or proper CA certificates +- Use secrets management for `MASTER_KEY` +- Enable MQTT authentication in `mosquitto.conf` diff --git a/deploy/laptop-border/bird.conf b/deploy/laptop-border/bird.conf new file mode 100644 index 0000000..441204f --- /dev/null +++ b/deploy/laptop-border/bird.conf @@ -0,0 +1,49 @@ +# BIRD - Border Router (AS 65000) +# Laptop n1: 172.30.0.100 (physical), 44.30.127.1 (Netmaker) +# Routes between ISP and Netmaker mesh + +router id 172.30.0.100; + +log syslog all; + +protocol device { + scan time 10; +} + +protocol kernel { + ipv4 { + import all; + export all; + }; +} + +# Direct routes - learn Netmaker interface +protocol direct { + ipv4; + interface "netmaker"; # Netmaker WireGuard interface +} + +# eBGP to ISP +protocol bgp isp { + description "ISP AS 65001"; + local 172.30.0.100 as 65000; + neighbor 172.30.0.1 as 65001; + + ipv4 { + import filter { + # Accept ISP test prefixes + if net ~ [192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24] then accept; + reject; + }; + + export filter { + # Announce our mesh network + if net ~ [44.30.127.0/24] then accept; + reject; + }; + }; + + hold time 90; + keepalive time 30; +} + diff --git a/deploy/laptop-border/docker-compose.yml b/deploy/laptop-border/docker-compose.yml new file mode 100644 index 0000000..44ed3e8 --- /dev/null +++ b/deploy/laptop-border/docker-compose.yml @@ -0,0 +1,99 @@ +# Border Router - Laptop n1 +# AS 65000 - BGP to ISP + Netmaker server + +services: + bird-border: + build: . + container_name: bird-border + hostname: border + cap_add: + - NET_ADMIN + volumes: + - ./bird.conf:/etc/bird/bird.conf:ro + network_mode: host # Port 179 exposed via host networking + restart: unless-stopped + + # Caddy reverse proxy (TLS termination for netmaker) + caddy: + image: caddy:2-alpine + container_name: caddy + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ./certs:/certs:ro + - caddy_data:/data + - caddy_config:/config + ports: + - "443:443" + depends_on: + - netmaker + restart: unless-stopped + + # Netmaker Server + netmaker: + image: gravitl/netmaker:v0.24.2 + container_name: netmaker + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + environment: + SERVER_NAME: "netmaker" + SERVER_HOST: "${SERVER_HOST:-172.30.0.100}" # Physical IP of this machine + SERVER_API_CONN_STRING: "${SERVER_HOST:-172.30.0.100}" + SERVER_HTTP_HOST: "${SERVER_HOST:-172.30.0.100}" + API_PORT: "8081" + BROKER_ENDPOINT: "mqtt://${SERVER_HOST:-172.30.0.100}:1883" + COREDNS_ADDR: "${SERVER_HOST:-172.30.0.100}" + MASTER_KEY: "${MASTER_KEY:-changeme}" + MQ_HOST: "${SERVER_HOST:-172.30.0.100}" + MQ_PORT: "1883" + DATABASE: "sqlite" + NODE_ID: "netmaker-server" + VERBOSITY: "3" + volumes: + - netmaker_data:/root/data + - netmaker_certs:/etc/netmaker + expose: + - "8081" + ports: + - "51821:51821/udp" # WireGuard + depends_on: + - mq + restart: unless-stopped + + # MQTT Broker for Netmaker + mq: + image: eclipse-mosquitto:2 + container_name: netmaker-mq + volumes: + - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro + - mq_data:/mosquitto/data + ports: + - "1883:1883" + restart: unless-stopped + + # Netclient on this node + # NOTE: net.ipv4.ip_forward=1 must be set on the host (sysctls not allowed with network_mode: host) + netclient: + image: gravitl/netclient:v0.24.2 + container_name: netclient + cap_add: + - NET_ADMIN + - SYS_MODULE + network_mode: host + volumes: + - netclient_data:/etc/netclient + - ./certs/server.crt:/etc/ssl/certs/netmaker.crt:ro + environment: + TOKEN: "${ENROLLMENT_TOKEN:-}" # Set after network creation + restart: unless-stopped + +volumes: + netmaker_data: + netmaker_certs: + mq_data: + netclient_data: + caddy_data: + caddy_config: diff --git a/deploy/laptop-border/entrypoint.sh b/deploy/laptop-border/entrypoint.sh new file mode 100644 index 0000000..a4567eb --- /dev/null +++ b/deploy/laptop-border/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail + +echo "=== BIRD BGP Daemon ===" +echo "Router ID: ${ROUTER_ID:-not set}" +echo "BGP AS: ${BGP_AS:-not set}" +mkdir -p /run/bird +chown bird:bird /run/bird +# Start BIRD in foreground +exec bird -f -c /etc/bird/bird.conf + diff --git a/deploy/laptop-border/mosquitto.conf b/deploy/laptop-border/mosquitto.conf new file mode 100644 index 0000000..48c11be --- /dev/null +++ b/deploy/laptop-border/mosquitto.conf @@ -0,0 +1,3 @@ +listener 1883 +allow_anonymous true + diff --git a/deploy/laptop-mesh/.gitignore b/deploy/laptop-mesh/.gitignore new file mode 100644 index 0000000..269d959 --- /dev/null +++ b/deploy/laptop-mesh/.gitignore @@ -0,0 +1,2 @@ +# Ignore local .env files +.env diff --git a/deploy/laptop-mesh/Dockerfile b/deploy/laptop-mesh/Dockerfile new file mode 100644 index 0000000..28df437 --- /dev/null +++ b/deploy/laptop-mesh/Dockerfile @@ -0,0 +1,8 @@ +FROM gravitl/netclient:v0.24.2 + +# Add custom CA certificate +COPY netmaker-ca.crt /usr/local/share/ca-certificates/netmaker-ca.crt + +# Install ca-certificates and update trust store +RUN apk add --no-cache ca-certificates && \ + update-ca-certificates diff --git a/deploy/laptop-mesh/SETUP.md b/deploy/laptop-mesh/SETUP.md new file mode 100644 index 0000000..73813aa --- /dev/null +++ b/deploy/laptop-mesh/SETUP.md @@ -0,0 +1,91 @@ +# Mesh Node Setup (Laptop n2) + +## Before deploying + +### 1. Install Netmaker CA certificate + +The Border Router uses a self-signed certificate. You must install it on this host: + +```bash +# Copy certificate from border router (172.30.0.100) +scp user@172.30.0.100:/path/to/deploy/laptop-border/certs/server.crt /tmp/netmaker.crt + +# Install certificate +sudo cp /tmp/netmaker.crt /usr/local/share/ca-certificates/netmaker.crt +sudo update-ca-certificates +``` + +### 2. Get enrollment token + +From the Border Router, get the enrollment token: + +```bash +curl -sk "https://172.30.0.100/api/v1/enrollment-keys" \ + -H "Authorization: Bearer $MASTER_KEY" +``` + +Copy the `token` field from the response. + +### 3. Create `.env` file + +```bash +echo "ENROLLMENT_TOKEN=" > .env +``` + +### 4. Enable IP forwarding on host + +```bash +sudo sysctl -w net.ipv4.ip_forward=1 +# Make persistent: +echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-ipforward.conf +``` + +### 5. Network requirements + +- UDP connectivity to Border Router on port 51821 (WireGuard) +- Can be on different LAN than other nodes (Netmaker handles NAT traversal) + +## Deploy + +```bash +docker compose up -d +``` + +## Verify + +```bash +# Check Netmaker client logs +docker logs netclient + +# Check WireGuard tunnel +docker exec netclient wg show + +# Check mesh connectivity +ping -c 3 44.30.127.1 # Border router mesh IP + +# Check received routes (from ISP via Border Router) +ip route | grep 192.0.2 # TEST-NET-1 +ip route | grep 198.51.100 # TEST-NET-2 +ip route | grep 203.0.113 # TEST-NET-3 +``` + +## How routes arrive + +1. ISP (AS 65001) announces test prefixes via BGP +2. Border Router (AS 65000) learns them via eBGP +3. Border Router announces mesh network (44.30.127.0/24) to ISP +4. Netmaker establishes WireGuard tunnel between nodes +5. Routes are distributed via the mesh + +## Troubleshooting + +### Certificate error +If you see `x509: certificate signed by unknown authority`: +- Ensure the CA certificate is installed (step 1) +- Run `update-ca-certificates` again + +### Registration failed +If netclient can't register: +- Check the Border Router's Caddy proxy is running: `curl -k https://172.30.0.100/api/server/health` +- Verify the enrollment token is correct +- Check firewall allows HTTPS (443) to Border Router diff --git a/deploy/laptop-mesh/docker-compose.yml b/deploy/laptop-mesh/docker-compose.yml new file mode 100644 index 0000000..ebd7c8c --- /dev/null +++ b/deploy/laptop-mesh/docker-compose.yml @@ -0,0 +1,20 @@ +# Mesh Node - Laptop n2 +# Netmaker client only (no BGP) + +services: + netclient: + build: . + container_name: netclient + cap_add: + - NET_ADMIN + - SYS_MODULE + # NOTE: net.ipv4.ip_forward=1 must be set on the host (sysctls not allowed with network_mode: host) + network_mode: host + volumes: + - netclient_data:/etc/netclient + environment: + TOKEN: "${ENROLLMENT_TOKEN}" # Get from Netmaker server API + restart: unless-stopped + +volumes: + netclient_data: diff --git a/deploy/laptop-mesh/netmaker-ca.crt b/deploy/laptop-mesh/netmaker-ca.crt new file mode 100644 index 0000000..44e8df9 --- /dev/null +++ b/deploy/laptop-mesh/netmaker-ca.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDIjCCAgqgAwIBAgIUDgDocWMIwvGG1Xj5qq5fLMHtaFIwDQYJKoZIhvcNAQEL +BQAwGDEWMBQGA1UEAwwNMTAuMTI4Ljk4LjEyNTAeFw0yNTEyMDQwMzU1MzRaFw0y +NjEyMDQwMzU1MzRaMBgxFjAUBgNVBAMMDTEwLjEyOC45OC4xMjUwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC85qMlYwTvrvkdevABSbbvwSZvGfFhti+5 +eT93+E74cdKYBU3aEt96vjwYaisWaXe5GBILmRv2ND8A/BwpMQSXeqHOlzVdyd+q +iKU90mVgOZywPnFv42QqTvvZxjYEAeSOS3pm0f9tWPPEqvNVLLJw6s2kh2nOBdRe +EiXV/3XCw7Vmis2V6dNLVy/VEXFeqHxfT3mUB3JkbalOtgwRRycGc8Q8tmJdbao/ +vkWSS6hW5FIkDL28+TwshjXPr1ct4scQmJ2H0hYsSdbwPcyiVFpSuZbSSZe9i/sv +ka9D/e+YhYZ0izo7m0Xo7CSZtUaakye/xo3laL6eC02SMPkLsvBJAgMBAAGjZDBi +MB0GA1UdDgQWBBSJG+4tCr3BVx3C9FoUnW5cdoe+tDAfBgNVHSMEGDAWgBSJG+4t +Cr3BVx3C9FoUnW5cdoe+tDAPBgNVHRMBAf8EBTADAQH/MA8GA1UdEQQIMAaHBAqA +Yn0wDQYJKoZIhvcNAQELBQADggEBAKEPLIJzQILa4nc6kj3ifxo7jfpVSw/4GkYl ++uLRpfxwDWWvC1v0CReol7vvysYRxytFvXvknITixoeBufH92Ry87c2wIDsL+zhR +itD5DE8ocgkwe82Nu2wW3Wm2kSrUESIpUk5eWlXFpU3poOK9uQvxgWvZfJwgcCeB +GJFpHJ8hHNQq+xwZo0JhqFeD0XVW/xGmPVdNWdWB1VcbHVss7jmbuFbIgRS7XpcL +lxDY6sSfsKbnBICWvkP/PxMAhOlU/2CkcuoEl7y+w8PGfzbQb72H8YIeoa9xyOFn +WelvRkUDCR18SHfErZyDyydw0IsKu76JTTpTHe4iWf44QEQWHVM= +-----END CERTIFICATE----- diff --git a/deploy/netmaker/.env.example b/deploy/netmaker/.env.example new file mode 100644 index 0000000..f83f319 --- /dev/null +++ b/deploy/netmaker/.env.example @@ -0,0 +1,2 @@ +SERVER_HOST=netmaker.example.com +MASTER_KEY=changeme diff --git a/deploy/netmaker/SETUP.md b/deploy/netmaker/SETUP.md new file mode 100644 index 0000000..189feac --- /dev/null +++ b/deploy/netmaker/SETUP.md @@ -0,0 +1,114 @@ +# Netmaker Server Setup (Core Components) + +Minimal deployment with Netmaker server and Mosquitto MQTT broker. + +## Components + +| Service | Purpose | Port | +|---------|---------|------| +| netmaker | Mesh VPN server | 8443 (HTTPS API) | +| mq | MQTT broker (Mosquitto) | 1883 | + +## Before deploying + +### 1. DNS Setup + +Point a domain to this server's public IP: +``` +netmaker.example.com -> YOUR_PUBLIC_IP +``` + +### 2. Create `.env` file + +```bash +cat < .env +SERVER_HOST=netmaker.example.com +MASTER_KEY=$(openssl rand -base64 32) +EOF +``` + +### 3. Firewall / Network requirements + +Open ports: +- 8443/TCP (Netmaker API) +- 51821/UDP (WireGuard) +- 1883/TCP (MQTT) + +## Deploy + +```bash +docker compose up -d +``` + +## Create Netmaker network + +```bash +source .env + +# Create network +curl -sk -X POST "https://${SERVER_HOST}:8443/api/networks" \ + -H "Authorization: Bearer $MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "netid": "mesh", + "addressrange": "44.30.127.0/24" + }' + +# Create enrollment key +curl -sk -X POST "https://${SERVER_HOST}:8443/api/v1/enrollment-keys" \ + -H "Authorization: Bearer $MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "networks": ["mesh"], + "tags": ["mesh-node"], + "unlimited": true + }' +``` + +Save the `token` field for client enrollment. + +## Verify + +```bash +docker compose ps +docker logs netmaker +docker logs netmaker-mq + +# API health +curl -sk https://${SERVER_HOST}:8443/api/server/health +``` + +## Enroll clients + +```bash +# Install netclient +curl -sL 'https://raw.githubusercontent.com/gravitl/netmaker/master/scripts/netclient-install.sh' | sudo VERSION=v0.24.2 bash + +# Join network +netclient join -t +``` + +## Architecture + +``` +┌─────────────────────────────────┐ +│ Netmaker Server │ +│ │ +│ ┌─────────┐ ┌───────────┐ │ +│ │Netmaker │ │ Mosquitto │ │ +│ │ :8443 │ │ :1883 │ │ +│ └────┬────┘ └───────────┘ │ +│ │ │ +│ :51821/UDP WireGuard │ +└─────────────────────────────────┘ + │ + ▼ + Mesh clients +``` + +## Security Note + +For production: +- Use secrets management for `MASTER_KEY` +- Enable MQTT authentication in `mosquitto.conf` +- Consider firewall rules to restrict MQTT access diff --git a/deploy/netmaker/docker-compose.yml b/deploy/netmaker/docker-compose.yml new file mode 100644 index 0000000..7cde30f --- /dev/null +++ b/deploy/netmaker/docker-compose.yml @@ -0,0 +1,52 @@ +# Netmaker Server - Core Components Only +# Provides mesh VPN infrastructure without BGP routing + +services: + # Netmaker Server + netmaker: + image: gravitl/netmaker:v0.24.2 + container_name: netmaker + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.ip_forward=1 + - net.ipv4.conf.all.src_valid_mark=1 + environment: + SERVER_NAME: "${SERVER_NAME:-netmaker}" + SERVER_HOST: "${SERVER_HOST}" + SERVER_API_CONN_STRING: "${SERVER_HOST}" + SERVER_HTTP_HOST: "${SERVER_HOST}" + API_PORT: "8443" + BROKER_ENDPOINT: "mqtt://${SERVER_HOST}:1883" + MASTER_KEY: "${MASTER_KEY}" + MQ_HOST: "${SERVER_HOST}" + MQ_PORT: "1883" + DATABASE: "sqlite" + NODE_ID: "netmaker-server" + VERBOSITY: "3" + volumes: + - netmaker_data:/root/data + - netmaker_certs:/etc/netmaker + ports: + - "8443:8443" # API (HTTPS) + - "51821:51821/udp" # WireGuard + depends_on: + - mq + restart: unless-stopped + + # MQTT Broker (Mosquitto) for Netmaker + mq: + image: eclipse-mosquitto:2 + container_name: netmaker-mq + volumes: + - ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro + - mq_data:/mosquitto/data + ports: + - "1883:1883" + restart: unless-stopped + +volumes: + netmaker_data: + netmaker_certs: + mq_data: diff --git a/deploy/netmaker/mosquitto.conf b/deploy/netmaker/mosquitto.conf new file mode 100644 index 0000000..c8348ac --- /dev/null +++ b/deploy/netmaker/mosquitto.conf @@ -0,0 +1,2 @@ +listener 1883 +allow_anonymous true diff --git a/deploy/rpi-isp/Dockerfile b/deploy/rpi-isp/Dockerfile new file mode 100644 index 0000000..681e8ad --- /dev/null +++ b/deploy/rpi-isp/Dockerfile @@ -0,0 +1,17 @@ +FROM debian:12-slim + +RUN apt-get update && apt-get install -y \ + bird2 \ + iproute2 \ + && rm -rf /var/lib/apt/lists/* + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 179 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s \ + CMD birdc show status || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] + diff --git a/deploy/rpi-isp/SETUP.md b/deploy/rpi-isp/SETUP.md new file mode 100644 index 0000000..7141479 --- /dev/null +++ b/deploy/rpi-isp/SETUP.md @@ -0,0 +1,77 @@ +# RPi ISP Setup (AS 65001) + +This is a mock ISP that announces test prefixes via BGP to the Border Router. + +## Before deploying + +### 1. Configure IPs + +Edit `bird.conf` and replace: +- `172.30.0.1` → Your Raspberry Pi's physical IP +- `172.30.0.100` → Border Router's physical IP (BGP neighbor) + +### 2. Network requirements + +- The RPi must be on the same LAN as the Border Router (Laptop n1) +- Port 179/TCP must be reachable (BGP) + +### 3. Use native Docker (not Docker Desktop) + +If using Docker Desktop on the RPi, `network_mode: host` won't work properly. +Use native Docker Engine: + +```bash +docker context use default +``` + +## Deploy + +```bash +docker compose up -d +``` + +## Verify + +```bash +# Check BIRD status +docker exec bird-isp birdc show status + +# Check BGP session (should show "Established") +docker exec bird-isp birdc show protocols + +# Check routes being announced +docker exec bird-isp birdc show route + +# Check routes received from Border Router +docker exec bird-isp birdc "show route protocol border_router" +``` + +## Test prefixes announced + +| Prefix | Description | +|--------|-------------| +| 192.0.2.0/24 | TEST-NET-1 (RFC 5737) | +| 198.51.100.0/24 | TEST-NET-2 (RFC 5737) | +| 203.0.113.0/24 | TEST-NET-3 (RFC 5737) | + +## Expected routes received + +Once the Border Router and mesh are up, you should receive: + +| Prefix | Description | +|--------|-------------| +| 44.30.127.0/24 | Netmaker mesh network | + +## Troubleshooting + +### BGP session not establishing +- Check both devices are on the same LAN +- Verify port 179 is not blocked by firewall +- Check IPs in bird.conf match actual interfaces + +### No routes received +- Verify Border Router has netclient running +- Check BIRD on Border Router sees the `netmaker` interface: + ```bash + docker exec bird-border birdc show protocols all direct1 + ``` diff --git a/deploy/rpi-isp/bird.conf b/deploy/rpi-isp/bird.conf new file mode 100644 index 0000000..bb55b23 --- /dev/null +++ b/deploy/rpi-isp/bird.conf @@ -0,0 +1,51 @@ +# BIRD - Mock ISP (AS 65001) +# Raspberry Pi: 172.30.0.1 +# Announces test prefixes to Border Router + +router id 172.30.0.1; + +log syslog all; + +protocol device { + scan time 10; +} + +protocol kernel { + ipv4 { + import none; + export all; + }; +} + +# Test prefixes to announce (RFC 5737 TEST-NET ranges) +protocol static isp_routes { + ipv4; + route 192.0.2.0/24 blackhole; # TEST-NET-1 + route 198.51.100.0/24 blackhole; # TEST-NET-2 + route 203.0.113.0/24 blackhole; # TEST-NET-3 +} + +# eBGP to Border Router +protocol bgp border_router { + description "Border Router AS 65000"; + local 172.30.0.1 as 65001; + neighbor 172.30.0.100 as 65000; + + ipv4 { + import filter { + # Accept mesh network announcements + if net ~ [44.30.127.0/24] then accept; + reject; + }; + + export filter { + # Announce our test prefixes + if proto = "isp_routes" then accept; + reject; + }; + }; + + hold time 90; + keepalive time 30; +} + diff --git a/deploy/rpi-isp/docker-compose.yml b/deploy/rpi-isp/docker-compose.yml new file mode 100644 index 0000000..ef01450 --- /dev/null +++ b/deploy/rpi-isp/docker-compose.yml @@ -0,0 +1,15 @@ +# Mock ISP - Raspberry Pi +# AS 65001 - Announces test prefixes to Border Router + +services: + bird-isp: + build: . + container_name: bird-isp + hostname: isp + cap_add: + - NET_ADMIN + volumes: + - ./bird.conf:/etc/bird/bird.conf:ro + network_mode: host # Uses host networking for BGP on physical interface + restart: unless-stopped + diff --git a/deploy/rpi-isp/entrypoint.sh b/deploy/rpi-isp/entrypoint.sh new file mode 100644 index 0000000..6e63083 --- /dev/null +++ b/deploy/rpi-isp/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail + +echo "=== BIRD BGP Daemon (Mock ISP) ===" +echo "Router ID: 172.30.0.1" +echo "BGP AS: 65001" + +mkdir -p /run/bird +chown bird:bird /run/bird + +# Start BIRD in foreground +exec bird -f -c /etc/bird/bird.conf + diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 89d1c7e..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,480 +0,0 @@ -version: '3.8' - -services: - bird1: - build: ./docker/bird - container_name: bird1 - network_mode: "service:tinc1" - volumes: - - ./configs/bird:/etc/bird:ro - environment: - - BGP_AS=${BGP_AS:-65000} - - ROUTER_ID=192.0.2.1 - - NODE_IP=10.0.0.1 - - NODE_ID=1 - - TOTAL_NODES=5 - restart: unless-stopped - depends_on: - - tinc1 - - bird2: - build: ./docker/bird - container_name: bird2 - network_mode: "service:tinc2" - volumes: - - ./configs/bird:/etc/bird:ro - environment: - - BGP_AS=${BGP_AS:-65000} - - ROUTER_ID=192.0.2.2 - - NODE_IP=10.0.0.2 - - NODE_ID=2 - - TOTAL_NODES=5 - restart: unless-stopped - depends_on: - - tinc2 - - bird3: - build: ./docker/bird - container_name: bird3 - network_mode: "service:tinc3" - volumes: - - ./configs/bird:/etc/bird:ro - environment: - - BGP_AS=${BGP_AS:-65000} - - ROUTER_ID=192.0.2.3 - - NODE_IP=10.0.0.3 - - NODE_ID=3 - - TOTAL_NODES=5 - restart: unless-stopped - depends_on: - - tinc3 - - bird4: - build: ./docker/bird - container_name: bird4 - network_mode: "service:tinc4" - volumes: - - ./configs/bird:/etc/bird:ro - environment: - - BGP_AS=${BGP_AS:-65000} - - ROUTER_ID=192.0.2.4 - - NODE_IP=10.0.0.4 - - NODE_ID=4 - - TOTAL_NODES=5 - restart: unless-stopped - depends_on: - - tinc4 - - bird5: - build: ./docker/bird - container_name: bird5 - network_mode: "service:tinc5" - volumes: - - ./configs/bird:/etc/bird:ro - environment: - - BGP_AS=${BGP_AS:-65000} - - ROUTER_ID=192.0.2.5 - - NODE_IP=10.0.0.5 - - NODE_ID=5 - - TOTAL_NODES=5 - restart: unless-stopped - depends_on: - - tinc5 - - daemon1: - build: - context: . - dockerfile: ./docker/go-daemon/Dockerfile - container_name: daemon1 - network_mode: "service:tinc1" - pid: "service:tinc1" - volumes: - - tinc1-data:/var/run/tinc - environment: - - NODE_NAME=node1 - - NODE_IP=10.0.0.1 - - TINC_ENDPOINT=tinc1:655 - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - - >- - ETCD_ENDPOINTS=etcd1:2379,etcd2:2379,etcd3:2379, - etcd4:2379,etcd5:2379 - command: - - /usr/local/bin/bgp-daemon - - -node=node1 - - -tinc-net=bgpmesh - - -etcd=etcd1:2379 - - -v - restart: unless-stopped - depends_on: - - tinc1 - - etcd1 - - daemon2: - build: - context: . - dockerfile: ./docker/go-daemon/Dockerfile - container_name: daemon2 - network_mode: "service:tinc2" - pid: "service:tinc2" - volumes: - - tinc2-data:/var/run/tinc - environment: - - NODE_NAME=node2 - - NODE_IP=10.0.0.2 - - TINC_ENDPOINT=tinc2:655 - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - - >- - ETCD_ENDPOINTS=etcd1:2379,etcd2:2379,etcd3:2379, - etcd4:2379,etcd5:2379 - command: - - /usr/local/bin/bgp-daemon - - -node=node2 - - -tinc-net=bgpmesh - - -etcd=etcd2:2379 - - -v - restart: unless-stopped - depends_on: - - tinc2 - - etcd2 - - daemon3: - build: - context: . - dockerfile: ./docker/go-daemon/Dockerfile - container_name: daemon3 - network_mode: "service:tinc3" - pid: "service:tinc3" - volumes: - - tinc3-data:/var/run/tinc - environment: - - NODE_NAME=node3 - - NODE_IP=10.0.0.3 - - TINC_ENDPOINT=tinc3:655 - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - - >- - ETCD_ENDPOINTS=etcd1:2379,etcd2:2379,etcd3:2379, - etcd4:2379,etcd5:2379 - command: - - /usr/local/bin/bgp-daemon - - -node=node3 - - -tinc-net=bgpmesh - - -etcd=etcd3:2379 - - -v - restart: unless-stopped - depends_on: - - tinc3 - - etcd3 - - daemon4: - build: - context: . - dockerfile: ./docker/go-daemon/Dockerfile - container_name: daemon4 - network_mode: "service:tinc4" - pid: "service:tinc4" - volumes: - - tinc4-data:/var/run/tinc - environment: - - NODE_NAME=node4 - - NODE_IP=10.0.0.4 - - TINC_ENDPOINT=tinc4:655 - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - - >- - ETCD_ENDPOINTS=etcd1:2379,etcd2:2379,etcd3:2379, - etcd4:2379,etcd5:2379 - command: - - /usr/local/bin/bgp-daemon - - -node=node4 - - -tinc-net=bgpmesh - - -etcd=etcd4:2379 - - -v - restart: unless-stopped - depends_on: - - tinc4 - - etcd4 - - daemon5: - build: - context: . - dockerfile: ./docker/go-daemon/Dockerfile - container_name: daemon5 - network_mode: "service:tinc5" - pid: "service:tinc5" - volumes: - - tinc5-data:/var/run/tinc - environment: - - NODE_NAME=node5 - - NODE_IP=10.0.0.5 - - TINC_ENDPOINT=tinc5:655 - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - - >- - ETCD_ENDPOINTS=etcd1:2379,etcd2:2379,etcd3:2379, - etcd4:2379,etcd5:2379 - command: - - /usr/local/bin/bgp-daemon - - -node=node5 - - -tinc-net=bgpmesh - - -etcd=etcd5:2379 - - -v - restart: unless-stopped - depends_on: - - tinc5 - - etcd5 - - tinc1: - build: ./docker/tinc - container_name: tinc1 - hostname: tinc1 - cap_add: - - NET_ADMIN - devices: - - /dev/net/tun - ports: - - "655:655/udp" - - "179:179" # BGP port (bird1 shares this network) - volumes: - - ./configs/tinc:/etc/tinc:ro - - tinc1-data:/var/run/tinc - depends_on: - - etcd1 - networks: - - mesh-net - - cluster-net - environment: - - TINC_NAME=node1 - - TINC_PORT=${TINC_PORT:-655} - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - restart: unless-stopped - - tinc2: - build: ./docker/tinc - container_name: tinc2 - hostname: tinc2 - cap_add: - - NET_ADMIN - devices: - - /dev/net/tun - volumes: - - ./configs/tinc:/etc/tinc:ro - - tinc2-data:/var/run/tinc - depends_on: - - etcd1 - networks: - - mesh-net - - cluster-net - environment: - - TINC_NAME=node2 - - TINC_PORT=${TINC_PORT:-655} - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - restart: unless-stopped - - tinc3: - build: ./docker/tinc - container_name: tinc3 - hostname: tinc3 - cap_add: - - NET_ADMIN - devices: - - /dev/net/tun - volumes: - - ./configs/tinc:/etc/tinc:ro - - tinc3-data:/var/run/tinc - depends_on: - - etcd1 - networks: - - mesh-net - - cluster-net - environment: - - TINC_NAME=node3 - - TINC_PORT=${TINC_PORT:-655} - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - restart: unless-stopped - - tinc4: - build: ./docker/tinc - container_name: tinc4 - hostname: tinc4 - cap_add: - - NET_ADMIN - devices: - - /dev/net/tun - ports: - - "656:655/udp" - volumes: - - ./configs/tinc:/etc/tinc:ro - - tinc4-data:/var/run/tinc - depends_on: - - etcd1 - networks: - - mesh-net - - cluster-net - environment: - - TINC_NAME=node4 - - TINC_PORT=${TINC_PORT:-655} - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - restart: unless-stopped - - tinc5: - build: ./docker/tinc - container_name: tinc5 - hostname: tinc5 - cap_add: - - NET_ADMIN - devices: - - /dev/net/tun - ports: - - "657:655/udp" - volumes: - - ./configs/tinc:/etc/tinc:ro - - tinc5-data:/var/run/tinc - depends_on: - - etcd1 - networks: - - mesh-net - - cluster-net - environment: - - TINC_NAME=node5 - - TINC_PORT=${TINC_PORT:-655} - - TINC_NETNAME=${TINC_NETNAME:-bgpmesh} - restart: unless-stopped - - etcd1: - image: quay.io/coreos/etcd:v3.5.14 - container_name: etcd1 - hostname: etcd1 - command: - - etcd - - --name=etcd1 - - --data-dir=/etcd-data - - --listen-client-urls=http://0.0.0.0:2379 - - --advertise-client-urls=http://etcd1:2379 - - --listen-peer-urls=http://0.0.0.0:2380 - - --initial-advertise-peer-urls=http://etcd1:2380 - - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380,etcd5=http://etcd5:2380 - - --initial-cluster-state=new - ports: - - "2379:2379" - - "2380:2380" - volumes: - - etcd1-data:/etcd-data - networks: - - cluster-net - - mesh-net - restart: unless-stopped - - etcd2: - image: quay.io/coreos/etcd:v3.5.14 - container_name: etcd2 - hostname: etcd2 - command: - - etcd - - --name=etcd2 - - --data-dir=/etcd-data - - --listen-client-urls=http://0.0.0.0:2379 - - --advertise-client-urls=http://etcd2:2379 - - --listen-peer-urls=http://0.0.0.0:2380 - - --initial-advertise-peer-urls=http://etcd2:2380 - - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380,etcd5=http://etcd5:2380 - - --initial-cluster-state=new - volumes: - - etcd2-data:/etcd-data - networks: - - cluster-net - restart: unless-stopped - - etcd3: - image: quay.io/coreos/etcd:v3.5.14 - container_name: etcd3 - hostname: etcd3 - command: - - etcd - - --name=etcd3 - - --data-dir=/etcd-data - - --listen-client-urls=http://0.0.0.0:2379 - - --advertise-client-urls=http://etcd3:2379 - - --listen-peer-urls=http://0.0.0.0:2380 - - --initial-advertise-peer-urls=http://etcd3:2380 - - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380,etcd5=http://etcd5:2380 - - --initial-cluster-state=new - volumes: - - etcd3-data:/etcd-data - networks: - - cluster-net - restart: unless-stopped - - etcd4: - image: quay.io/coreos/etcd:v3.5.14 - container_name: etcd4 - hostname: etcd4 - command: - - etcd - - --name=etcd4 - - --data-dir=/etcd-data - - --listen-client-urls=http://0.0.0.0:2379 - - --advertise-client-urls=http://etcd4:2379 - - --listen-peer-urls=http://0.0.0.0:2380 - - --initial-advertise-peer-urls=http://etcd4:2380 - - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380,etcd5=http://etcd5:2380 - - --initial-cluster-state=new - volumes: - - etcd4-data:/etcd-data - networks: - - cluster-net - restart: unless-stopped - - etcd5: - image: quay.io/coreos/etcd:v3.5.14 - container_name: etcd5 - hostname: etcd5 - command: - - etcd - - --name=etcd5 - - --data-dir=/etcd-data - - --listen-client-urls=http://0.0.0.0:2379 - - --advertise-client-urls=http://etcd5:2379 - - --listen-peer-urls=http://0.0.0.0:2380 - - --initial-advertise-peer-urls=http://etcd5:2380 - - --initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380,etcd4=http://etcd4:2380,etcd5=http://etcd5:2380 - - --initial-cluster-state=new - volumes: - - etcd5-data:/etcd-data - networks: - - cluster-net - restart: unless-stopped - - prometheus: - build: ./docker/monitoring - container_name: prometheus - hostname: prometheus - ports: - - "9090:9090" - - "3000:3000" - volumes: - - ./configs/prometheus:/etc/prometheus:ro - - ./configs/grafana/provisioning:/etc/grafana/provisioning:ro - - ./configs/grafana/dashboards:/etc/grafana/dashboards:ro - networks: - - mesh-net - restart: unless-stopped - -networks: - mesh-net: - driver: bridge - ipam: - config: - - subnet: 172.20.0.0/16 - cluster-net: - driver: bridge - internal: true - -volumes: - etcd1-data: - etcd2-data: - etcd3-data: - etcd4-data: - etcd5-data: - tinc1-data: - tinc2-data: - tinc3-data: - tinc4-data: - tinc5-data: diff --git a/docker/bird/Dockerfile b/docker/bird/Dockerfile deleted file mode 100644 index 23a668e..0000000 --- a/docker/bird/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM debian:12-slim - -# Install BIRD 3.x and dependencies -RUN apt-get update && apt-get install -y \ - bird2 \ - python3-jinja2 \ - iproute2 \ - curl \ - procps \ - && rm -rf /var/lib/apt/lists/* - -# Note: bird2 package provides BIRD 3.x in Debian 12 -# Verify version -RUN bird --version || echo "BIRD installed" - -# Copy entrypoint script -COPY entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh - -# Expose BGP port -EXPOSE 179 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD birdc show status || exit 1 - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/docker/bird/entrypoint.sh b/docker/bird/entrypoint.sh deleted file mode 100755 index a200f77..0000000 --- a/docker/bird/entrypoint.sh +++ /dev/null @@ -1,107 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "============================================" -echo "BIRD BGP Daemon - Entrypoint" -echo "============================================" - -# Environment variables with defaults -ROUTER_ID="${ROUTER_ID:-192.0.2.1}" -BGP_AS="${BGP_AS:-65000}" -NODE_IP="${NODE_IP:-10.0.0.1}" -NODE_ID="${NODE_ID:-1}" -TOTAL_NODES="${TOTAL_NODES:-5}" - -echo "Configuration:" -echo " Router ID: $ROUTER_ID" -echo " BGP AS: $BGP_AS" -echo " Node IP: $NODE_IP" -echo " Node ID: $NODE_ID" -echo " Total Nodes: $TOTAL_NODES" -echo "" - -# Create writable config directory -mkdir -p /var/run/bird - -# Render BIRD configuration from template -if [ -f /etc/bird/bird.conf.j2 ]; then - echo "Rendering bird.conf from template..." - python3 << EOF -from jinja2 import Template -import sys - -with open('/etc/bird/bird.conf.j2', 'r') as f: - template = Template(f.read()) - -output = template.render(router_id='$ROUTER_ID', bgp_as='$BGP_AS') - -with open('/var/run/bird/bird.conf', 'w') as f: - f.write(output) -EOF - - # Render protocols.conf from template - if [ -f /etc/bird/protocols.conf.j2 ]; then - echo "Rendering protocols.conf from template..." - python3 << 'PROTOCOLS_EOF' -from jinja2 import Template -import sys -import os - -node_ip = os.environ.get('NODE_IP', '10.0.0.1') -node_id = int(os.environ.get('NODE_ID', '1')) -bgp_as = os.environ.get('BGP_AS', '65000') -total_nodes = int(os.environ.get('TOTAL_NODES', '5')) - -with open('/etc/bird/protocols.conf.j2', 'r') as f: - template = Template(f.read()) - -output = template.render( - node_ip=node_ip, - node_id=node_id, - bgp_as=bgp_as, - total_nodes=total_nodes -) - -with open('/var/run/bird/protocols.conf', 'w') as f: - f.write(output) -PROTOCOLS_EOF - echo "✓ protocols.conf rendered" - else - # Fallback to static file if template doesn't exist - cp /etc/bird/protocols.conf /var/run/bird/ 2>/dev/null || true - fi - - # Copy static filters.conf to writable location - cp /etc/bird/filters.conf /var/run/bird/ 2>/dev/null || true - - # Update include paths in rendered config - sed -i 's|/etc/bird/|/var/run/bird/|g' /var/run/bird/bird.conf - - echo "✓ Configuration rendered" -else - echo "⚠ No template found, using static config" - # Copy static config if exists - if [ -f /etc/bird/bird.conf ]; then - cp /etc/bird/* /var/run/bird/ - fi -fi - -# Validate BIRD configuration (BIRD 2.x doesn't have --parse-only) -echo "" -echo "Skipping config validation (BIRD 2.x limitation)" -echo "Configuration will be validated on startup" - -# Display configuration files -echo "" -echo "Configuration files:" -echo " - /var/run/bird/bird.conf" -echo " - /var/run/bird/protocols.conf" -echo " - /var/run/bird/filters.conf" - -# Start BIRD in foreground with debug output -echo "" -echo "Starting BIRD daemon..." -echo "============================================" - -# Use exec to replace shell with bird process (PID 1) -exec bird -f -c /var/run/bird/bird.conf diff --git a/docker/go-daemon/Dockerfile b/docker/go-daemon/Dockerfile deleted file mode 100644 index e49843b..0000000 --- a/docker/go-daemon/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -# Build stage -FROM golang:1.23-alpine AS builder - -# Install build dependencies -RUN apk add --no-cache git make - -# Set working directory -WORKDIR /build - -# Copy go.mod and go.sum first for better caching -COPY daemon-go/go.mod daemon-go/go.sum ./ -RUN go mod download - -# Copy source code -COPY daemon-go/ ./ - -# Build the daemon -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o bgp-daemon ./cmd/bgp-daemon - -# Runtime stage -FROM alpine:latest - -# Install runtime dependencies -# tinc package provides tincd binary needed for reload via pidfile -RUN apk add --no-cache \ - tinc \ - ca-certificates \ - bash - -# Create tinc directories -RUN mkdir -p /var/run/tinc - -# Copy daemon binary from builder -COPY --from=builder /build/bgp-daemon /usr/local/bin/bgp-daemon - -# Make it executable -RUN chmod +x /usr/local/bin/bgp-daemon - -# Default command -CMD ["/usr/local/bin/bgp-daemon"] diff --git a/docker/monitoring/Dockerfile b/docker/monitoring/Dockerfile deleted file mode 100644 index 07456f6..0000000 --- a/docker/monitoring/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM prom/prometheus:v2.53.1 AS prom - -FROM grafana/grafana:11.2.0 - -# Copy prometheus binary from prometheus image -COPY --from=prom /bin/prometheus /usr/local/bin/prometheus -COPY --from=prom /etc/prometheus/prometheus.yml /etc/prometheus/prometheus.yml.default - -# Install supervisor to run both services -USER root -RUN apk add --no-cache supervisor curl - -# Copy entrypoint script -COPY entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh - -# Create supervisor config -RUN mkdir -p /etc/supervisor/conf.d - -# Expose ports -EXPOSE 9090 3000 - -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD curl -f http://localhost:9090/-/healthy && curl -f http://localhost:3000/api/health || exit 1 - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/docker/monitoring/entrypoint.sh b/docker/monitoring/entrypoint.sh deleted file mode 100755 index c0d60d5..0000000 --- a/docker/monitoring/entrypoint.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "============================================" -echo "Monitoring Stack - Entrypoint" -echo "============================================" -echo "Starting Prometheus + Grafana" -echo "" - -# Create prometheus data directory -mkdir -p /prometheus -chown -R nobody:nobody /prometheus - -# Use custom prometheus config if provided -if [ -f /etc/prometheus/prometheus.yml ]; then - echo "✓ Using custom Prometheus configuration" - PROM_CONFIG="/etc/prometheus/prometheus.yml" -else - echo "⚠ Using default Prometheus configuration" - PROM_CONFIG="/etc/prometheus/prometheus.yml.default" -fi - -# Start Prometheus in background -echo "" -echo "Starting Prometheus on port 9090..." -prometheus \ - --config.file="$PROM_CONFIG" \ - --storage.tsdb.path=/prometheus \ - --web.console.libraries=/usr/share/prometheus/console_libraries \ - --web.console.templates=/usr/share/prometheus/consoles \ - --web.listen-address=0.0.0.0:9090 \ - & - -PROM_PID=$! -echo "✓ Prometheus started (PID: $PROM_PID)" - -# Wait a bit for Prometheus to start -sleep 5 - -# Start Grafana in foreground -echo "" -echo "Starting Grafana on port 3000..." -echo "============================================" - -# Set Grafana defaults -export GF_PATHS_PROVISIONING=/etc/grafana/provisioning -export GF_SECURITY_ADMIN_PASSWORD="${GRAFANA_ADMIN_PASSWORD:-admin}" -export GF_USERS_ALLOW_SIGN_UP=false - -cd /usr/share/grafana - -# Trap signals to cleanup -trap "kill $PROM_PID" SIGTERM SIGINT - -# Start Grafana -exec /run.sh diff --git a/docker/tinc/Dockerfile b/docker/tinc/Dockerfile deleted file mode 100644 index 071db37..0000000 --- a/docker/tinc/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM debian:12-slim - -# Install TINC 1.0 and dependencies -RUN apt-get update && apt-get install -y \ - tinc \ - iproute2 \ - python3-jinja2 \ - curl \ - procps \ - etcd-client \ - && rm -rf /var/lib/apt/lists/* - -# Pin TINC 1.0 to prevent upgrades -RUN apt-mark hold tinc - -# Verify TINC version -RUN tincd --version || echo "TINC installed" - -# Copy entrypoint script -COPY entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh - -# Expose TINC UDP port -EXPOSE 655/udp - -# Health check - verify tinc0 interface exists -HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD ip addr show tinc0 || exit 1 - -ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/docker/tinc/entrypoint.sh b/docker/tinc/entrypoint.sh deleted file mode 100755 index a6a1787..0000000 --- a/docker/tinc/entrypoint.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "============================================" -echo "TINC VPN Mesh - Entrypoint" -echo "============================================" - -# Environment variables with defaults -TINC_NAME="${TINC_NAME:-node1}" -TINC_PORT="${TINC_PORT:-655}" -TINC_NETNAME="${TINC_NETNAME:-bgpmesh}" - -# Extract node number from name (node1 → 1) -NODE_ID="${TINC_NAME: -1}" - -echo "Configuration:" -echo " Node name: $TINC_NAME" -echo " Node ID: $NODE_ID" -echo " Port: $TINC_PORT" -echo " Network: $TINC_NETNAME" -echo "" - -# Create TINC directory structure in writable location -TINC_DIR="/var/run/tinc/$TINC_NETNAME" -mkdir -p "$TINC_DIR/hosts" - -# Generate RSA keys if they don't exist -if [ ! -f "$TINC_DIR/rsa_key.priv" ]; then - echo "Generating RSA 2048-bit keys..." - cd "$TINC_DIR" - # Generate keys using correct config path - echo -e "\n\n" | tincd -c "$TINC_DIR" -K2048 - echo "✓ RSA keys generated" -else - echo "✓ Using existing RSA keys" -fi - -# Always create host file (regenerate on each start) -if [ -f "$TINC_DIR/rsa_key.pub" ]; then - echo "Creating host file..." - # Use Docker service name for address resolution (tinc1, tinc2, tinc3) - CONTAINER_NAME="tinc$NODE_ID" - - cat > "$TINC_DIR/hosts/$TINC_NAME" << EOF -# Host configuration for $TINC_NAME -Address = $CONTAINER_NAME -Port = $TINC_PORT -Subnet = 10.0.0.$NODE_ID/32 - -EOF - cat "$TINC_DIR/rsa_key.pub" >> "$TINC_DIR/hosts/$TINC_NAME" - echo "✓ Host file created (Address = $CONTAINER_NAME)" -fi - -# Render TINC configuration from template (only if it doesn't exist) -if [ -f /etc/tinc/tinc.conf.j2 ] && [ ! -f "$TINC_DIR/tinc.conf" ]; then - echo "" - echo "Rendering tinc.conf from template..." - python3 << EOF -from jinja2 import Template - -with open('/etc/tinc/tinc.conf.j2', 'r') as f: - template = Template(f.read()) - -output = template.render(tinc_name='$TINC_NAME', tinc_port='$TINC_PORT') - -with open('$TINC_DIR/tinc.conf', 'w') as f: - f.write(output) -EOF - echo "✓ tinc.conf rendered" - - # No bootstrap ConnectTo directives - daemon will manage connections dynamically - echo "" - echo "⚠ No initial ConnectTo directives" - echo " Daemon will manage connections dynamically via CLI" - echo "✓ Bootstrap configuration ready" -elif [ -f "$TINC_DIR/tinc.conf" ]; then - echo "✓ Using existing tinc.conf" -fi - -# Render tinc-up script -if [ -f /etc/tinc/tinc-up.j2 ]; then - echo "Rendering tinc-up script..." - python3 << EOF -from jinja2 import Template - -with open('/etc/tinc/tinc-up.j2', 'r') as f: - template = Template(f.read()) - -output = template.render(tinc_name='$TINC_NAME', node_id='$NODE_ID', hostname='$(hostname)') - -with open('$TINC_DIR/tinc-up', 'w') as f: - f.write(output) -EOF - chmod +x "$TINC_DIR/tinc-up" - echo "✓ tinc-up rendered and executable" -fi - -# Render tinc-down script -if [ -f /etc/tinc/tinc-down.j2 ]; then - echo "Rendering tinc-down script..." - python3 << EOF -from jinja2 import Template - -with open('/etc/tinc/tinc-down.j2', 'r') as f: - template = Template(f.read()) - -output = template.render(tinc_name='$TINC_NAME') - -with open('$TINC_DIR/tinc-down', 'w') as f: - f.write(output) -EOF - chmod +x "$TINC_DIR/tinc-down" - echo "✓ tinc-down rendered and executable" -fi - -# Display configuration -echo "" -echo "Configuration files:" -echo " - $TINC_DIR/tinc.conf" -echo " - $TINC_DIR/tinc-up" -echo " - $TINC_DIR/tinc-down" -echo " - $TINC_DIR/rsa_key.priv" - -# Start TINC daemon -echo "" -echo "Starting TINC daemon..." -echo "============================================" - -# Start tincd in foreground mode -# -D = no daemon, stay in foreground (required for Docker) -# -d3 = debug level 3 -# --logfile = write logs to file for debugging -# Using full config path -exec tincd -c /var/run/tinc/"$TINC_NETNAME" -D -d3 --logfile="/var/run/tinc/$TINC_NETNAME/tinc.log" diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md deleted file mode 100644 index d8a76c6..0000000 --- a/docs/DEPLOYMENT.md +++ /dev/null @@ -1,484 +0,0 @@ -# Deployment Guide - -## Docker Compose (Development/Testing) - -### Requirements - -- Docker 24+ with Compose v2 -- >8GB RAM (>16GB for 5-node) -- Linux kernel with TUN/TAP support - -### Local Deployment (Sprint 1.5 - 5 nodes) - -```bash -git clone https://github.com/pablomonte/bgp-network.git -cd bgp-network -cp .env.example .env -make deploy-local -``` - -Wait ~90-120s for convergence (5-node mesh). - -**Architecture**: - -`docker-compose.yml` deploys a full mesh with **21 containers**: -- 5x BIRD (BGP routing with dynamic peer configuration) -- 5x TINC (VPN mesh with Subnet declarations for layer 2) -- 5x etcd (distributed storage, 5-node cluster) -- 5x daemon (Go automation for peer propagation) -- 1x prometheus + grafana (monitoring) - -Each BIRD node automatically configures **N-1 peers** (4 peers for 5 nodes) using the `protocols.conf.j2` template with environment variables (`NODE_IP`, `NODE_ID`, `TOTAL_NODES`). - -### Verify Deployment - -```bash -docker ps # Should show 21 running -docker exec bird1 birdc show protocols # BGP sessions (expect 4/4 Established) -docker exec tinc1 ip addr show tinc0 # TINC interface -docker exec etcd1 etcdctl endpoint health # etcd cluster -curl http://localhost:2112/metrics # Daemon metrics (via tinc1) - -# Verify all nodes have correct peer counts (Sprint 1.5) -for i in {1..5}; do - echo "bird$i: $(docker exec bird$i birdc show protocols | grep -c Established) peers" -done -# Should show "4 peers" for each node -``` - -**Automated Key Distribution**: - -TINC keys are automatically distributed via etcd: -1. Each node generates RSA-2048 keys on startup -2. tinc-up script stores keys in etcd at `/peers/` -3. Go daemon syncs keys and updates ConnectTo directives -4. TINC daemon reloads with new peers - -No manual bootstrap required. Nodes auto-discover and connect. - -### Configuration - -Edit `.env`: - -```bash -BGP_AS=65000 -TINC_NETNAME=bgpmesh -TINC_PORT=655 -``` - -Restart affected services (all 5 nodes): - -```bash -docker restart bird1 bird2 bird3 bird4 bird5 -# Or restart all bird containers at once -docker restart $(docker ps -q -f name=bird) -``` - -### Monitoring - -- **Prometheus**: http://localhost:9090 -- **Grafana**: http://localhost:3000 (admin/admin) - -Dashboard: BGP Daemon Overview - -### Cleanup - -```bash -make clean # Stop containers -docker compose down -v # Remove volumes (deletes data) -``` - ---- - -## Ansible (Production) - -### Requirements - -**Control node**: -- Ansible 2.16+ -- Python 3.8+ -- SSH access to targets - -**Target nodes** (each): -- Ubuntu 22.04+ or Debian 12+ -- >2GB RAM, >10GB disk -- Root or sudo access - -### Setup - -#### 1. Inventory - -```bash -cd ansible -cp inventory/hosts.ini.example inventory/hosts.ini -``` - -Edit `inventory/hosts.ini`: - -```ini -[bgp_nodes] -node1 ansible_host=192.168.1.101 node_ip=10.0.0.1 router_id=192.0.2.1 -node2 ansible_host=192.168.1.102 node_ip=10.0.0.2 router_id=192.0.2.2 -node3 ansible_host=192.168.1.103 node_ip=10.0.0.3 router_id=192.0.2.3 -node4 ansible_host=192.168.1.104 node_ip=10.0.0.4 router_id=192.0.2.4 -node5 ansible_host=192.168.1.105 node_ip=10.0.0.5 router_id=192.0.2.5 - -[bgp_nodes:vars] -ansible_user=root -``` - -#### 2. Variables - -Edit `group_vars/all.yml`: - -```yaml -bgp_as: 65000 -tinc_netname: "bgpmesh" -etcd_version: "3.5.14" -``` - -#### 3. Build Daemon - -```bash -cd daemon-go -make build -# Binary at bin/bgp-daemon -``` - -#### 4. Test Connectivity - -```bash -cd ../ansible -ansible -i inventory/hosts.ini all -m ping -``` - -### Deployment - -Full deploy (all roles, all nodes): - -```bash -ansible-playbook -i inventory/hosts.ini playbook.yml -``` - -Duration: ~10-15 min for 5 nodes - -### Partial Deployment - -Specific roles: - -```bash -ansible-playbook -i inventory/hosts.ini playbook.yml --tags tinc,bird -ansible-playbook -i inventory/hosts.ini playbook.yml --tags daemon -``` - -Specific host: - -```bash -ansible-playbook -i inventory/hosts.ini playbook.yml --limit node1 -``` - -### Verification - -#### All Services - -```bash -ansible -i inventory/hosts.ini all -m shell -a "systemctl status etcd bird tinc@bgpmesh bgp-daemon --no-pager" -``` - -#### etcd Cluster - -```bash -ssh root@node1 -etcdctl member list -etcdctl endpoint health -``` - -Expected: -``` -http://10.0.0.1:2379 is healthy: successfully committed proposal: took = 3.2ms -http://10.0.0.2:2379 is healthy: successfully committed proposal: took = 3.8ms -... -``` - -#### TINC Mesh - -```bash -ssh root@node1 -ip addr show tinc0 -ping -c 3 10.0.0.2 -tinc -n bgpmesh info -``` - -#### BGP Sessions - -```bash -ssh root@node1 -birdc show protocols -birdc show route -``` - -Expected: -``` -Name Proto Table State -peer2 BGP --- up Established -peer3 BGP --- up Established -... -``` - -#### Go Daemon - -```bash -ssh root@node1 -systemctl status bgp-daemon -curl http://localhost:2112/metrics -``` - ---- - -## Configuration - -### BIRD - -Edit `ansible/roles/bird/templates/bird.conf.j2` - -Redeploy: - -```bash -ansible-playbook -i inventory/hosts.ini playbook.yml --tags bird -``` - -### TINC - -Edit `ansible/roles/tinc/templates/tinc.conf.j2` - -Redeploy: - -```bash -ansible-playbook -i inventory/hosts.ini playbook.yml --tags tinc -``` - -### Go Daemon - -Rebuild binary: - -```bash -cd daemon-go && make build -``` - -Redeploy: - -```bash -cd ../ansible -ansible-playbook -i inventory/hosts.ini playbook.yml --tags daemon -``` - ---- - -## Scaling - -### Add Node - -#### 1. Update Inventory - -Add to `inventory/hosts.ini`: - -```ini -node6 ansible_host=192.168.1.106 node_ip=10.0.0.6 router_id=192.0.2.6 -``` - -#### 2. Add to etcd Cluster - -From existing node: - -```bash -ssh root@node1 -etcdctl member add etcd6 --peer-urls=http://10.0.0.6:2380 -``` - -Update `etcd_cluster_members` in group_vars to include node6. - -#### 3. Deploy - -```bash -ansible-playbook -i inventory/hosts.ini playbook.yml --limit node6 -``` - -#### 4. Verify - -```bash -etcdctl member list -ssh root@node6 ip addr show tinc0 -ssh root@node6 birdc show protocols -``` - -### Remove Node - -```bash -ssh root@node6 -systemctl stop bgp-daemon bird tinc@bgpmesh etcd -``` - -From other node: - -```bash -etcdctl member remove -``` - -Remove from inventory. - ---- - -## Backup/Restore - -### etcd Backup - -Manual: - -```bash -ssh root@node1 -etcdctl snapshot save /backup/etcd-$(date +%Y%m%d).db -etcdctl snapshot status /backup/etcd-$(date +%Y%m%d).db -``` - -Automated (cron): - -```bash -# /etc/cron.daily/etcd-backup -#!/bin/bash -etcdctl snapshot save /backup/etcd-$(date +%Y%m%d).db -find /backup -name "etcd-*.db" -mtime +7 -delete -``` - -### etcd Restore - -Stop all etcd nodes: - -```bash -ansible -i inventory/hosts.ini all -m systemd -a "name=etcd state=stopped" -``` - -Restore on each node: - -```bash -ssh root@node1 -etcdctl snapshot restore /backup/etcd-20251028.db \ - --name node1 \ - --initial-cluster "node1=http://10.0.0.1:2380,node2=http://10.0.0.2:2380,..." \ - --initial-advertise-peer-urls http://10.0.0.1:2380 \ - --data-dir /var/lib/etcd-restore - -mv /var/lib/etcd /var/lib/etcd-old -mv /var/lib/etcd-restore /var/lib/etcd -``` - -Start all: - -```bash -ansible -i inventory/hosts.ini all -m systemd -a "name=etcd state=started" -``` - -### TINC Keys Backup - -```bash -tar czf tinc-keys-$(date +%Y%m%d).tar.gz /etc/tinc/bgpmesh/rsa_key.priv /etc/tinc/bgpmesh/rsa_key.pub -``` - -Restore: - -```bash -tar xzf tinc-keys-20251028.tar.gz -C / -systemctl restart tinc@bgpmesh -``` - ---- - -## Troubleshooting - -### etcd Won't Start - -Check logs: - -```bash -systemctl status etcd -journalctl -xe -u etcd -``` - -Common causes: -- Port 2379/2380 in use: `netstat -tlpn | grep -E "2379|2380"` -- Permissions: `chown -R etcd:etcd /var/lib/etcd` -- Bad config: `cat /etc/etcd/etcd.conf` - -### TINC Not Connecting - -Check status: - -```bash -tinc -n bgpmesh info -tinc -n bgpmesh dump nodes -journalctl -u tinc@bgpmesh -``` - -Common causes: -- Missing peer host files: `ls /etc/tinc/bgpmesh/hosts/` -- Firewall blocking UDP 655: `ufw allow 655/udp` -- Wrong ConnectTo: check `/etc/tinc/bgpmesh/tinc.conf` - -Debug: - -```bash -tinc -n bgpmesh --debug=5 -``` - -### BGP Sessions Not Establishing - -Check: - -```bash -birdc show protocols all peer1 -journalctl -u bird -``` - -Common causes: -- TINC mesh not connected: `ping 10.0.0.2` -- Wrong neighbor IP: check `/etc/bird/protocols.conf` -- Firewall blocking TCP 179: `iptables -L -n | grep 179` - -Validate config: - -```bash -bird -p -c /etc/bird/bird.conf -``` - -### Go Daemon Crashes - -Check logs: - -```bash -systemctl status bgp-daemon -journalctl -u bgp-daemon -f -``` - -Common causes: -- etcd unreachable: `etcdctl endpoint health` -- TINC permissions: `chown -R bgp-daemon:bgp-daemon /var/run/tinc` -- Binary version mismatch: `/opt/bgp-daemon/bgp-daemon -version` - -### High etcd Latency - -Check performance: - -```bash -etcdctl check perf -``` - -Fixes: -- Use SSD for `/var/lib/etcd` -- Reduce network latency between nodes -- Increase resources (CPU, RAM) - -### BGP Slow Convergence - -Fixes: -- Enable BFD: set `bgp_bfd_enabled: true` in `group_vars/all.yml` -- Reduce BGP timers in BIRD config -- Check TINC overhead: `ping -c 100 10.0.0.2` diff --git a/docs/MANUAL_TESTING.md b/docs/MANUAL_TESTING.md deleted file mode 100644 index 80c2c12..0000000 --- a/docs/MANUAL_TESTING.md +++ /dev/null @@ -1,892 +0,0 @@ -# Manual Testing - Guía de Debugging Paso a Paso - -Esta guía replica manualmente lo que hace el workflow de CI para debuggear problemas de integración. - -**Actualizado para Sprint 1.5** (Enero 2025) - -- ✅ 5 nodos en full mesh topology (antes 3) -- ✅ BGP peers generados dinámicamente desde templates (protocols.conf.j2) -- ✅ TINC con Subnet declarations para layer 2 (fix de ARP) -- ✅ Tests escalables con detección automática de node count -- ✅ Pre-commit hooks configurados (gofmt, go vet, tests) - -## Arquitectura Sprint 1.5 - Cambios Clave - -### Dynamic BGP Peer Configuration - -En lugar de hardcodear peers en `protocols.conf`, ahora usamos Jinja2 templates: - -- **Template**: `configs/bird/protocols.conf.j2` -- **Variables**: `NODE_IP`, `NODE_ID`, `TOTAL_NODES` (desde docker-compose.yml) -- **Resultado**: Cada nodo genera N-1 peers automáticamente (full mesh) - -Ejemplo para node1 (NODE_IP=10.0.0.1, TOTAL_NODES=5): - -```conf -protocol bgp peer1 { local 10.0.0.1 as 65000; neighbor 10.0.0.2 as 65000; } -protocol bgp peer2 { local 10.0.0.1 as 65000; neighbor 10.0.0.3 as 65000; } -protocol bgp peer3 { local 10.0.0.1 as 65000; neighbor 10.0.0.4 as 65000; } -protocol bgp peer4 { local 10.0.0.1 as 65000; neighbor 10.0.0.5 as 65000; } -``` - -### TINC Layer 2 Fix - -Agregamos `Subnet = IP/32` en host files para correcta resolución ARP: - -- **Sin Subnet**: ARP muestra ``, ping falla -- **Con Subnet**: ARP muestra `REACHABLE`, ping 100% exitoso - -### Pre-Commit Hooks - -Instalados en `.git/hooks/pre-commit` para prevenir CI failures: - -1. ✅ Go formatting (`gofmt -s`) -2. ✅ Go vet (static analysis) -3. ✅ Unit tests - -Instalar en nuevos clones: `./scripts/install-hooks.sh` - -## Prerequisitos - -```bash -# Verificar herramientas -docker --version # >= 24.0 -docker compose version # v2.x -etcdctl version # Para queries manuales a etcd -go version # >= 1.23 (para daemon-go development) - -# Si falta etcdctl: -ETCD_VER=v3.5.14 -wget https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz -tar xzf etcd-${ETCD_VER}-linux-amd64.tar.gz -sudo mv etcd-${ETCD_VER}-linux-amd64/etcdctl /usr/local/bin/ - -# Instalar pre-commit hooks (opcional pero recomendado) -./scripts/install-hooks.sh -``` - -## Paso 1: Preparación - -```bash -cd /home/pablo/repos/BGP - -# Limpiar ejecución anterior -docker compose down -v -docker volume prune -f - -# Copiar .env -cp .env.example .env - -# Ver configuración -cat .env -``` - -## Paso 2: Build de Imágenes - -```bash -# Build con output detallado -docker compose build --no-cache --progress=plain - -# Verificar imágenes creadas -docker images | grep bgp4mesh - -# Deberías ver: -# bgp4mesh-bird -# bgp4mesh-tinc -# bgp4mesh-daemon -# bgp4mesh-prometheus -``` - -## Paso 3: Levantar Servicios - -```bash -# Levantar con logs en foreground -docker compose up - -# O en background para seguir trabajando: -docker compose up -d - -# Ver logs de todos los servicios -docker compose logs -f -``` - -## Paso 4: Verificar Contenedores - -```bash -# Listar todos los contenedores -docker compose ps - -# Deberías ver 21 contenedores "Up": -# - tinc1, tinc2, tinc3, tinc4, tinc5 (5) -# - bird1, bird2, bird3, bird4, bird5 (5) -# - daemon1, daemon2, daemon3, daemon4, daemon5 (5) -# - etcd1, etcd2, etcd3, etcd4, etcd5 (5) -# - prometheus (1) - -# Verificar que estén healthy -docker compose ps | grep healthy - -# Si alguno no está Up, ver logs: -docker logs tinc1 -docker logs bird1 -docker logs daemon1 -``` - -## Paso 5: Verificar etcd (Clave!) - -### 5.1 Verificar Cluster - -```bash -# Ver miembros del cluster -docker exec etcd1 etcdctl member list - -# Verificar salud -docker exec etcd1 etcdctl endpoint health - -# Ver estado detallado -docker exec etcd1 etcdctl endpoint status --write-out=table -``` - -### 5.2 Verificar Registro de Peers - -```bash -# Listar todas las keys de peers -docker exec etcd1 etcdctl get /peers --prefix --keys-only - -# Deberías ver: -# /peers/node1 -# /peers/node2 -# /peers/node3 -# /peers/node4 -# /peers/node5 - -# Ver contenido completo de un peer -docker exec etcd1 etcdctl get /peers/node1 - -# Debería mostrar JSON con: -# {"ip":"10.0.0.1","endpoint":"node1:655","key":"-----BEGIN RSA PUBLIC KEY-----\n..."} -``` - -### 5.3 Verificar Timing de Registro - -```bash -# Ver logs de TINC para ver cuándo se registró -docker logs tinc1 | grep -E "interface configured|Waiting for" - -# Ver logs de daemon para ver cuándo sincronizó -docker logs daemon1 | grep -E "Stored own key|Synced host file" -``` - -## Paso 6: Verificar TINC (Debugging Detallado) - -### 6.1 Verificar Configuración de TINC - -```bash -# Ver tinc.conf de node1 -docker exec tinc1 cat /var/run/tinc/bgpmesh/tinc.conf - -# Debería mostrar: -# Name = node1 -# Mode = switch -# Port = 655 -# ConnectTo = node2 # Bootstrap topology - hardcoded in entrypoint.sh -# ConnectTo = node3 # Node1 and node2 use hardcoded ConnectTo for initial mesh - -# Ver tinc.conf de node2 -docker exec tinc2 cat /var/run/tinc/bgpmesh/tinc.conf - -# Debería mostrar: -# Name = node2 -# Mode = switch -# Port = 655 -# ConnectTo = node1 # Bootstrap node - -# Nota: A partir de node3, no hay ConnectTo hardcodeados -# Los nodos 3-5 se conectan dinámicamente vía peer discovery - -# Verificar configuración en todos los nodos -for i in {1..5}; do - echo "=== Node $i ===" - docker exec tinc$i cat /var/run/tinc/bgpmesh/tinc.conf | grep -E "Name|Mode|Port|ConnectTo" -done -``` - -### 6.2 Verificar Host Files (CRÍTICO!) - -```bash -# Ver host files en tinc1 -docker exec tinc1 ls -la /var/run/tinc/bgpmesh/hosts/ - -# Deberían existir 5 archivos: node1, node2, node3, node4, node5 - -# Si faltan archivos, el problema está aquí! -# Verificar contenido de un host file -docker exec tinc1 cat /var/run/tinc/bgpmesh/hosts/node2 - -# Debería mostrar (Sprint 1.5 - con Subnet declaration): -# # Host configuration for node2 -# Address = node2 -# Port = 655 -# Subnet = 10.0.0.2/32 -# -# -----BEGIN RSA PUBLIC KEY----- -# MIIBCgKCAQEA... -# -----END RSA PUBLIC KEY----- - -# IMPORTANTE: La línea "Subnet = 10.0.0.X/32" es CRÍTICA para layer 2 -# Sin ella, ARP resolution falla y el ping no funciona - -# Verificar en todos los nodos -for i in {1..5}; do - echo "=== Tinc $i - Host Files ===" - docker exec tinc$i ls /var/run/tinc/bgpmesh/hosts/ | wc -l - docker exec tinc$i ls /var/run/tinc/bgpmesh/hosts/ -done -``` - -### 6.3 Verificar Proceso tincd - -```bash -# Ver procesos tincd corriendo -for i in {1..5}; do - echo "=== Tinc $i ===" - docker exec tinc$i ps aux | grep tincd | grep -v grep -done - -# Ver si tincd está en foreground (-D) o background -docker exec tinc1 ps aux | grep "tincd -D" -``` - -### 6.4 Verificar Interfaces de Red - -```bash -# Ver interface tinc0 en cada nodo -for i in {1..5}; do - echo "=== Tinc $i Interface ===" - docker exec tinc$i ip addr show tinc0 -done - -# Cada uno debería mostrar: -# tinc0: mtu 1400 -# inet 10.0.0.X/24 -# inet6 2001:db8::X/64 - -# Ver rutas -docker exec tinc1 ip route -``` - -### 6.5 Verificar ARP Resolution (Sprint 1.5 - Crítico) - -```bash -# Verificar tabla ARP en tinc1 -docker exec tinc1 ip neigh show dev tinc0 - -# Debería mostrar REACHABLE para todos los peers: -# 10.0.0.2 lladdr XX:XX:XX:XX:XX:XX REACHABLE -# 10.0.0.3 lladdr XX:XX:XX:XX:XX:XX REACHABLE -# 10.0.0.4 lladdr XX:XX:XX:XX:XX:XX REACHABLE -# 10.0.0.5 lladdr XX:XX:XX:XX:XX:XX REACHABLE - -# Si muestra "", falta la declaración Subnet en host files! -# Verificar que TODOS los host files tienen Subnet: -for i in {1..5}; do - echo "=== node$i host file ===" - docker exec tinc1 grep "Subnet" /var/run/tinc/bgpmesh/hosts/node$i -done -``` - -### 6.6 Test de Conectividad TINC (LA PRUEBA DEFINITIVA) - -```bash -# Desde tinc1, ping a todos los demás -echo "=== Ping from tinc1 to all nodes ===" -for i in {2..5}; do - echo -n "tinc1 -> 10.0.0.$i: " - docker exec tinc1 ping -c 3 -W 2 10.0.0.$i >/dev/null 2>&1 && echo "✓ OK" || echo "✗ FAIL" -done - -# Si FALLA el ping, AQUÍ está el problema! -# Diagnosticar: - -# 1. Ver si tincd está intentando conectar -docker logs tinc1 2>&1 | tail -50 - -# 2. Ver conexiones de red activas -docker exec tinc1 netstat -tupn | grep tincd - -# 3. Intentar conexión manual (si tinc CLI disponible) -docker exec tinc1 sh -c 'tinc -n bgpmesh dump nodes 2>/dev/null' || echo "tinc CLI not available" - -# 4. Verificar que existe el host file del peer -docker exec tinc1 ls -la /var/run/tinc/bgpmesh/hosts/node2 - -# 5. Ver si hay mensajes de error en logs -docker logs tinc1 2>&1 | grep -i "error\|fail\|timeout\|refused" -``` - -### 6.7 Timing Analysis - -```bash -# Ver el orden temporal de eventos -echo "=== TINC1 Timeline ===" -docker logs tinc1 2>&1 | grep -E "Generating|rendered|ConnectTo|Waiting for host|Starting TINC|configured" - -# Esto debería mostrar: -# 1. Generating RSA keys -# 2. tinc.conf rendered -# 3. Bootstrap topology configured (ConnectTo directives added) -# 4. Waiting for host file propagation (10s) -# 5. Starting TINC daemon -# 6. interface configured -``` - -### 6.8 Full Mesh Ping Validation (Sprint 1.5) - -```bash -# Test completo de conectividad full mesh (N×(N-1) pairs) -# Para 5 nodos = 20 pings totales - -echo "=== Full Mesh Connectivity Test ===" -TOTAL=0 -SUCCESS=0 - -for src in {1..5}; do - for dst in {1..5}; do - if [ "$src" != "$dst" ]; then - TOTAL=$((TOTAL + 1)) - if docker exec tinc$src ping -c 1 -W 2 10.0.0.$dst >/dev/null 2>&1; then - SUCCESS=$((SUCCESS + 1)) - echo "✓ tinc$src -> 10.0.0.$dst" - else - echo "✗ tinc$src -> 10.0.0.$dst FAILED" - fi - fi - done -done - -echo "" -echo "Result: $SUCCESS/$TOTAL pings successful" - -# Para 5 nodos, deberías ver: 20/20 pings successful -``` - -## Paso 7: Verificar Daemon Go - -```bash -# Ver logs del daemon1 -docker logs daemon1 | tail -80 - -# Buscar mensajes clave en orden: -docker logs daemon1 | grep -E "Connecting to etcd|Connected to etcd|Stored own key|Synced host file|etcd PUT" - -# Debería mostrar: -# 1. ✓ Connected to etcd -# 2. ✓ TINC manager initialized -# 3. ✓ Read local public key (XXX bytes) -# 4. ✓ Stored own key in etcd at /peers/node1 -# 5. ✓ Synced host file for peer (para cada peer) -# 6. 📥 etcd PUT: /peers/nodeX (cuando otros nodos se registran) - -# Ver si el daemon está recibiendo eventos de etcd -docker logs daemon1 | grep "📥 etcd PUT" - -# Si NO hay mensajes "etcd PUT", el daemon no está viendo los otros nodos! -# Verificar conexión a etcd: -docker exec daemon1 nc -zv etcd1 2379 -``` - -## Paso 8: Verificar BIRD (BGP) - -### 8.1 Verificar Configuración - -```bash -# Ver configuración principal de BIRD1 -docker exec bird1 cat /etc/bird/bird.conf | head -30 - -# Ver configuración de peers (generada dinámicamente desde template) -docker exec bird1 cat /var/run/bird/protocols.conf - -# Deberías ver N-1 peers (para 5 nodos, 4 peers): -# protocol bgp peer1 { -# description "BGP peer at 10.0.0.2"; -# local 10.0.0.1 as 65000; -# neighbor 10.0.0.2 as 65000; -# ... -# } -# protocol bgp peer2 { ... } -# protocol bgp peer3 { ... } -# protocol bgp peer4 { ... } - -# IMPORTANTE: protocols.conf se genera desde protocols.conf.j2 -# usando variables de entorno NODE_IP, NODE_ID, TOTAL_NODES - -# Verificar que cada nodo tiene su propia configuración única -for i in {1..5}; do - echo "=== Bird $i - Local IP ===" - docker exec bird$i grep "local 10.0.0" /var/run/bird/protocols.conf | head -1 -done - -# Ver estado general -docker exec bird1 birdc show status -``` - -### 8.2 Verificar Template Rendering (Sprint 1.5) - -```bash -# Ver variables de entorno usadas para rendering -docker exec bird1 env | grep -E "NODE_IP|NODE_ID|TOTAL_NODES|BGP_AS" - -# Deberías ver: -# NODE_IP=10.0.0.1 -# NODE_ID=1 -# TOTAL_NODES=5 -# BGP_AS=65000 - -# Verificar que el template se renderizó correctamente -docker exec bird1 ls -la /var/run/bird/protocols.conf - -# Ver el contenido generado -docker exec bird1 cat /var/run/bird/protocols.conf | head -50 - -# Contar cuántos peers se generaron -docker exec bird1 grep -c "protocol bgp peer" /var/run/bird/protocols.conf - -# Debería ser N-1 (para 5 nodos = 4 peers) - -# Verificar que cada nodo tiene diferentes IPs locales -for i in {1..5}; do - echo -n "bird$i local IP: " - docker exec bird$i grep "local 10.0.0" /var/run/bird/protocols.conf | head -1 | awk '{print $2}' -done - -# Deberías ver: -# bird1 local IP: 10.0.0.1 -# bird2 local IP: 10.0.0.2 -# bird3 local IP: 10.0.0.3 -# bird4 local IP: 10.0.0.4 -# bird5 local IP: 10.0.0.5 -``` - -### 8.3 Verificar Protocolos BGP (LA PRUEBA FINAL) - -```bash -# Ver todos los protocolos en bird1 -docker exec bird1 birdc show protocols - -# Debería mostrar: -# Name Proto Table State Since Info -# device1 Device --- up HH:MM:SS -# kernel1 Kernel master4 up HH:MM:SS -# static1 Static master4 up HH:MM:SS -# peer1 BGP --- up HH:MM:SS Established -# peer2 BGP --- up HH:MM:SS Established -# peer3 BGP --- up HH:MM:SS Established -# peer4 BGP --- up HH:MM:SS Established - -# Contar sesiones establecidas -docker exec bird1 birdc show protocols | grep -c "Established" - -# Ver detalles de una sesión específica -docker exec bird1 birdc show protocols all peer1 - -# Si el estado es "start" o "Active" en lugar de "Established": -docker exec bird1 birdc show protocols all peer1 | grep -A 5 "BGP state" - -# Los errores comunes: -# - "Socket: No route to host" -> TINC no conectó! -# - "Socket: Connection refused" -> BIRD del peer no está escuchando -# - "Active" -> Intentando conectar (puede ser timing) -``` - -### 8.4 Full Mesh BGP Validation (Sprint 1.5) - -```bash -# Verificar que TODOS los nodos tienen 4/4 sesiones BGP establecidas -echo "=== Full Mesh BGP Session Validation ===" - -for i in {1..5}; do - ESTABLISHED=$(docker exec bird$i birdc show protocols 2>/dev/null | grep -c "Established" || echo "0") - EXPECTED=4 # N-1 para 5 nodos - - if [ "$ESTABLISHED" -eq "$EXPECTED" ]; then - echo "✓ bird$i: $ESTABLISHED/$EXPECTED sessions established" - else - echo "✗ bird$i: $ESTABLISHED/$EXPECTED sessions (INCOMPLETE)" - docker exec bird$i birdc show protocols - fi -done - -# Resultado esperado para 5 nodos: -# ✓ bird1: 4/4 sessions established -# ✓ bird2: 4/4 sessions established -# ✓ bird3: 4/4 sessions established -# ✓ bird4: 4/4 sessions established -# ✓ bird5: 4/4 sessions established -# -# Total: 20 sesiones BGP (5 nodos × 4 peers cada uno) -``` - -### 8.5 Diagnóstico de Problemas BGP - -```bash -# Si BGP no establece: - -# 1. SIEMPRE verificar TINC primero -docker exec bird1 ping -c 3 10.0.0.2 - -# 2. Verificar que BIRD está escuchando en puerto 179 -docker exec bird1 netstat -tuln | grep 179 - -# 3. Ver logs de BIRD -docker logs bird1 | grep -i "bgp\|peer\|error" - -# 4. Test de conectividad TCP al peer -docker exec bird1 nc -zv 10.0.0.2 179 - -# 5. Ver configuración de peer en BIRD -docker exec bird1 cat /etc/bird/bird.conf | grep -A 10 "protocol bgp peer1" -``` - -## Paso 9: Script de Test Completo - -```bash -# Crear script de test automatizado -cat > /tmp/manual_test.sh << 'TESTSCRIPT' -#!/bin/bash -set -e - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -echo "=========================================" -echo " Manual Integration Test" -echo "=========================================" -echo "" - -# Test 1: Containers -echo "Test 1: Verificando contenedores..." -RUNNING=$(docker compose ps --format json 2>/dev/null | jq -r 'select(.State == "running") | .Name' | wc -l) -if [ "$RUNNING" -eq 21 ]; then - echo -e "${GREEN}✓${NC} $RUNNING/21 containers running" -else - echo -e "${RED}✗${NC} Solo $RUNNING/21 containers running" - docker compose ps - exit 1 -fi -echo "" - -# Test 2: etcd -echo "Test 2: Verificando etcd..." -docker exec etcd1 etcdctl endpoint health >/dev/null 2>&1 || { echo -e "${RED}✗${NC} etcd not healthy"; exit 1; } -PEERS=$(docker exec etcd1 etcdctl get /peers --prefix --keys-only 2>/dev/null | wc -l) -if [ "$PEERS" -eq 5 ]; then - echo -e "${GREEN}✓${NC} etcd healthy, $PEERS peers registered" -else - echo -e "${YELLOW}⚠${NC} etcd healthy but only $PEERS/5 peers" -fi -echo "" - -# Test 3: TINC Host Files -echo "Test 3: Verificando TINC host files..." -ALL_OK=true -for i in {1..5}; do - HOSTS=$(docker exec tinc$i ls /var/run/tinc/bgpmesh/hosts/ 2>/dev/null | wc -l) - if [ "$HOSTS" -eq 5 ]; then - echo -e " ${GREEN}✓${NC} tinc$i has 5 host files" - else - echo -e " ${RED}✗${NC} tinc$i has only $HOSTS host files" - docker exec tinc$i ls /var/run/tinc/bgpmesh/hosts/ - ALL_OK=false - fi -done -$ALL_OK || { echo "Host files missing!"; exit 1; } -echo "" - -# Test 4: TINC Connectivity -echo "Test 4: Verificando conectividad TINC..." -ALL_OK=true -for i in {2..5}; do - if docker exec tinc1 ping -c 2 -W 2 10.0.0.$i >/dev/null 2>&1; then - echo -e " ${GREEN}✓${NC} tinc1 -> 10.0.0.$i" - else - echo -e " ${RED}✗${NC} tinc1 -> 10.0.0.$i FAILED" - ALL_OK=false - fi -done -$ALL_OK || { echo "TINC mesh not connected!"; exit 1; } -echo "" - -# Test 5: BGP Sessions -echo "Test 5: Verificando sesiones BGP..." -ALL_OK=true -EXPECTED=4 # Para 5 nodos, cada uno tiene 4 peers (full mesh) -for i in {1..5}; do - ESTABLISHED=$(docker exec bird$i birdc show protocols 2>/dev/null | grep -c "Established" || echo "0") - if [ "$ESTABLISHED" -eq "$EXPECTED" ]; then - echo -e " ${GREEN}✓${NC} bird$i: $ESTABLISHED/$EXPECTED BGP sessions established" - else - echo -e " ${YELLOW}⚠${NC} bird$i: $ESTABLISHED/$EXPECTED BGP sessions (incomplete)" - docker exec bird$i birdc show protocols - ALL_OK=false - fi -done -$ALL_OK || { echo "BGP sessions not fully established"; exit 1; } -echo "" - -echo "=========================================" -echo -e "${GREEN}✓ All tests passed!${NC}" -echo "=========================================" -TESTSCRIPT - -chmod +x /tmp/manual_test.sh - -# Ejecutar test -/tmp/manual_test.sh -``` - -## Paso 10: Análisis de Timing - -```bash -# Este script muestra el timeline de eventos para diagnosticar timing issues -cat > /tmp/timing_analysis.sh << 'TIMING' -#!/bin/bash - -echo "=== TIMING ANALYSIS ===" -echo "" - -echo "--- Node1 Timeline ---" -docker logs tinc1 2>&1 | grep -E "Configuration:|Generating|rendered|ConnectTo|Waiting|Starting|configured" | head -20 - -echo "" -echo "--- Daemon1 Timeline ---" -docker logs daemon1 2>&1 | grep -E "Starting|Connected|Stored own key|Synced|etcd PUT" | head -15 - -echo "" -echo "--- Bird1 Timeline ---" -docker logs bird1 2>&1 | grep -E "Started|Listening|BGP" | head -10 - -echo "" -echo "--- etcd Registration Times ---" -for i in {1..5}; do - echo -n "node$i registered: " - docker logs daemon$i 2>&1 | grep "Stored own key" | head -1 | cut -d' ' -f1-2 -done -TIMING - -chmod +x /tmp/timing_analysis.sh -/tmp/timing_analysis.sh -``` - -## Troubleshooting Común - -### Problema: Host files no se sincronizan - -```bash -# Verificar que el daemon puede leer la clave pública -docker exec daemon1 cat /var/run/tinc/bgpmesh/rsa_key.pub - -# Verificar que puede escribir en hosts/ -docker exec daemon1 touch /var/run/tinc/bgpmesh/hosts/test_write -docker exec daemon1 rm /var/run/tinc/bgpmesh/hosts/test_write - -# Ver si el daemon recibe eventos de etcd -docker logs daemon1 | grep "etcd PUT" | tail -10 - -# Ver si hay errores al escribir host files -docker logs daemon1 | grep -i "error\|fail" -``` - -### Problema: TINC no conecta después de tener host files - -```bash -# Ver logs completos de TINC (últimas 100 líneas) -docker logs tinc1 2>&1 | tail -100 - -# Si no hay output de conexión, TINC puede estar corriendo sin debug -# Verificar cómo se inició tincd: -docker exec tinc1 ps aux | grep tincd - -# Si se inició con -D (foreground), los logs deberían estar en docker logs -# Si se inició sin -D, puede estar daemonizado sin logs - -# Intentar ver el estado interno de TINC (si tinc CLI está disponible): -docker exec tinc1 sh -c 'command -v tinc && tinc -n bgpmesh dump nodes' || echo "tinc CLI not in PATH" -``` - -### Problema: Timing - Containers arrancan en orden incorrecto - -```bash -# Ver el orden de arranque -docker compose ps --format "{{.Name}} {{.Status}}" - -# Verificar depends_on en docker-compose.yml -grep -A 5 "depends_on:" docker-compose.yml - -# Si los daemons arrancan antes que TINC genere claves: -docker logs daemon1 | head -20 - -# Deberías ver: "⚠ Failed to read local key" si arranca muy temprano -# Solución: Agregar health checks o delays -``` - -### Problema Sprint 1.5: Template rendering falló - -```bash -# Verificar que el template existe -docker exec bird1 ls -la /etc/bird/protocols.conf.j2 - -# Verificar variables de entorno -docker exec bird1 env | grep -E "NODE_|TOTAL_|BGP_" - -# Verificar que protocols.conf se generó -docker exec bird1 ls -la /var/run/bird/protocols.conf - -# Si no existe, ver logs de entrypoint -docker logs bird1 | grep -i "rendering\|template\|jinja" - -# Verificar Python y Jinja2 están instalados -docker exec bird1 which python3 -docker exec bird1 python3 -c "import jinja2; print(jinja2.__version__)" - -# Re-generar manualmente para debugging -docker exec bird1 python3 << 'EOF' -from jinja2 import Template -import os - -with open('/etc/bird/protocols.conf.j2', 'r') as f: - template = Template(f.read()) - -output = template.render( - node_ip=os.environ.get('NODE_IP', '10.0.0.1'), - node_id=int(os.environ.get('NODE_ID', '1')), - bgp_as=os.environ.get('BGP_AS', '65000'), - total_nodes=int(os.environ.get('TOTAL_NODES', '5')) -) -print(output) -EOF -``` - -### Problema Sprint 1.5: ARP muestra incomplete (falta Subnet) - -```bash -# Verificar tabla ARP -docker exec tinc1 ip neigh show dev tinc0 - -# Si muestra "", verificar host files -for i in {1..5}; do - echo "=== node$i ===" - docker exec tinc1 cat /var/run/tinc/bgpmesh/hosts/node$i | grep -E "Address|Port|Subnet" -done - -# Debería mostrar para cada host: -# Address = nodeX -# Port = 655 -# Subnet = 10.0.0.X/32 - -# Si falta Subnet, verificar que manager.go tiene la línea: -# content := fmt.Sprintf(`... -# Subnet = %s/32 -# ...`, peer.IP.String()) - -# Re-sync forzado (si el daemon está corriendo) -docker restart daemon1 daemon2 daemon3 daemon4 daemon5 - -# Esperar 10s y verificar de nuevo -sleep 10 -docker exec tinc1 ip neigh show dev tinc0 -``` - -### Problema Sprint 1.5: BGP tiene menos peers de los esperados - -```bash -# Verificar cuántos peers se generaron en el config -docker exec bird1 grep -c "protocol bgp peer" /var/run/bird/protocols.conf - -# Debería ser N-1 (para 5 nodos = 4) - -# Verificar TOTAL_NODES env var -docker exec bird1 env | grep TOTAL_NODES - -# Si no está seteada o es incorrecta, verificar docker-compose.yml: -grep -A 5 "bird1:" docker-compose.yml | grep TOTAL_NODES - -# Debería tener: -# - TOTAL_NODES=5 - -# Si es incorrecto, editar docker-compose.yml y rebuild -docker compose up -d --build bird1 bird2 bird3 bird4 bird5 -``` - -## Limpieza - -```bash -# Detener todo -docker compose down - -# Limpiar volúmenes (borrar todas las claves y datos) -docker compose down -v - -# Limpiar todo incluyendo imágenes -docker compose down -v --rmi local -``` - -## Paso 11: Interpretar Tests Automatizados (Sprint 1.5) - -Los tests automatizados ahora se adaptan dinámicamente al número de nodos: - -```bash -# Ver el test automatizado de BGP peering -cat tests/integration/test_bgp_peering.sh | grep -A 20 "NODE_COUNT" - -# El test detecta automáticamente cuántos nodos hay: -NODE_COUNT=$(docker compose ps --services 2>/dev/null | grep -c "^bird" || echo 0) -EXPECTED_PEERS=$((NODE_COUNT - 1)) - -# Para 5 nodos: -# - NODE_COUNT = 5 -# - EXPECTED_PEERS = 4 -# - Total BGP sessions = 20 (5 × 4) -# - Total TINC pings = 20 (5 × 4) - -# Ejecutar el test completo -make test-integration - -# O directamente: -./tests/integration/test_bgp_peering.sh - -# Resultado esperado: -# ✓ 5/5 containers running -# ✓ etcd healthy, 5 peers registered -# ✓ BGP: 20/20 sessions established -# ✓ TINC: 20/20 pings successful -# ✓ Full mesh connectivity validated -``` - -### Thresholds Dinámicos - -Los tests ahora usan thresholds que escalan con NODE_COUNT: - -```bash -# Para N nodos, se verifican: -EXPECTED_CONTAINERS=$((N * 4 + 1)) # N×(tinc+bird+daemon+etcd) + prometheus -EXPECTED_BGP_SESSIONS=$((N * (N-1))) # Full mesh bidireccional -EXPECTED_PINGS=$((N * (N-1))) # Full mesh connectivity - -# Ejemplos: -# 3 nodos: 13 containers, 6 BGP sessions, 6 pings -# 5 nodos: 21 containers, 20 BGP sessions, 20 pings -# 10 nodos: 41 containers, 90 BGP sessions, 90 pings -``` - -## Tips de Debugging - -1. **Siempre verificar en orden**: etcd -> host files -> TINC ping -> ARP -> BGP -2. **Si BGP falla, NUNCA es BGP primero**: Siempre es TINC que no conectó -3. **Verificar Subnet declarations**: Sin `Subnet = IP/32`, ARP falla y ping no funciona -4. **Timing matters**: Esperar 20-30s después de `docker compose up` antes de testear (para 5 nodos) -5. **Logs son tu amigo**: `docker logs -f` en otra terminal mientras debuggeas -6. **Test incremental**: No testear BGP hasta que TINC ping funcione -7. **Pre-commit hooks**: Usar `./scripts/install-hooks.sh` para prevenir CI failures -8. **Dynamic config**: Verificar que `protocols.conf` se generó con N-1 peers correctos diff --git a/docs/NETMAKER.md b/docs/NETMAKER.md new file mode 100644 index 0000000..97045ac --- /dev/null +++ b/docs/NETMAKER.md @@ -0,0 +1,138 @@ +# Netmaker configuration + +## Components + +| Container | Image | Function | +|-----------|-------|----------| +| netmaker | gravitl/netmaker:v0.24.2 | Server, manages mesh topology | +| netmaker-mq | eclipse-mosquitto:2 | MQTT broker for node communication | +| caddy | caddy:2-alpine | TLS termination (HTTPS required by netclient) | +| netclient | gravitl/netclient:v0.24.2 | WireGuard client on each mesh node | + +## Network + +| Parameter | Value | +|-----------|-------| +| Network ID | mesh | +| Address range | 44.30.127.0/24 | +| WireGuard port | 51821/UDP | +| API port | 443/TCP (via Caddy) | +| MQTT port | 1883/TCP | + +## API + +### Authentication + +All API calls require the `Authorization: Bearer ` header. + +### Create network + +```bash +curl -sk -X POST "https:///api/networks" \ + -H "Authorization: Bearer $MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{"netid": "mesh", "addressrange": "44.30.127.0/24"}' +``` + +### Create enrollment key + +```bash +curl -sk -X POST "https:///api/v1/enrollment-keys" \ + -H "Authorization: Bearer $MASTER_KEY" \ + -H "Content-Type: application/json" \ + -d '{"networks": ["mesh"], "tags": ["node"], "unlimited": true}' +``` + +Response contains `token` field (base64-encoded JSON with server address and key value). + +### List hosts + +```bash +curl -sk "https:///api/hosts" \ + -H "Authorization: Bearer $MASTER_KEY" +``` + +### List networks + +```bash +curl -sk "https:///api/networks" \ + -H "Authorization: Bearer $MASTER_KEY" +``` + +### Health check + +```bash +curl -sk "https:///api/server/health" +``` + +## TLS requirement + +Netmaker v0.24.x netclient requires HTTPS. The project uses Caddy with self-signed certificates. + +Certificate generation: +```bash +openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout certs/server.key -out certs/server.crt \ + -subj "/CN=" -addext "subjectAltName=IP:" +``` + +Hosts running netclient must trust this certificate: +```bash +sudo cp server.crt /usr/local/share/ca-certificates/netmaker.crt +sudo update-ca-certificates +``` + +## Environment variables + +### Server (netmaker) + +| Variable | Description | +|----------|-------------| +| SERVER_HOST | Physical IP address | +| SERVER_API_CONN_STRING | `` (no scheme, no port) | +| SERVER_HTTP_HOST | `` (no scheme, no port) | +| API_PORT | Internal API port (8081) | +| BROKER_ENDPOINT | `mqtt://:1883` | +| MQ_HOST | MQTT broker IP | +| MQ_PORT | MQTT port (1883) | +| MASTER_KEY | API authentication key | +| DATABASE | `sqlite` | + +### Client (netclient) + +| Variable | Description | +|----------|-------------| +| TOKEN | Enrollment token from API | + +## WireGuard interface + +Netclient creates interface named `netmaker` (not `nm-*` as documented elsewhere). + +BIRD configuration must reference this interface: +``` +protocol direct { + ipv4; + interface "netmaker"; +} +``` + +## Troubleshooting + +### netclient: certificate signed by unknown authority + +Install the CA certificate on the host (not just in the container). + +### netclient: https://http//... + +Token contains `http://` in server field. Ensure `SERVER_HTTP_HOST` and `SERVER_API_CONN_STRING` do not include scheme. + +### BIRD not exporting mesh route + +BIRD must be restarted after the `netmaker` interface is created: +```bash +docker restart bird-border +``` + +### netmaker crash loop: could not connect to broker + +Check `BROKER_ENDPOINT` uses `mqtt://` scheme (not `ws://`). Mosquitto default config does not support WebSocket. diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md deleted file mode 100644 index fb3b516..0000000 --- a/docs/QUICKSTART.md +++ /dev/null @@ -1,305 +0,0 @@ -# Quickstart Guide - -## Prerequisites - -- **Docker 24+** with Compose v2 -- **Go 1.21+** for daemon development -- **Ansible 2.16+** for production deployment -- **>8GB RAM** (>16GB recommended for parallel builds) -- **Linux host** (Debian stable recommended, macOS with caveats) - -## Setup - -### 1. Clone and Configure - -```bash -cd ~/repos/BGP -cp .env.example .env -``` - -Edit `.env` to customize (optional): -```bash -vim .env -# Change BGP_AS=65001 if needed -# Change BIRD_PASSWORD for BGP sessions -``` - -### 2. Deploy Local 5-Node Mesh (Sprint 1.5) - -```bash -make deploy-local -``` - -This will: -- Build 4 Docker images (BIRD, TINC, daemon, prometheus) -- Start 21 containers (5 bird + 5 tinc + 5 daemon + 5 etcd + 1 prometheus) -- Bootstrap etcd cluster (5 nodes) -- Generate TINC keys with Subnet declarations (layer 2 fix) -- Configure BGP sessions dynamically (N-1 peers per node via templates) - -**Wait ~90-120 seconds** for convergence (5 nodes take longer than 3). - -### 3. Verify - -```bash -# Check all containers are running -docker ps - -# Check BGP sessions (Sprint 1.5: dynamic peers) -docker exec bird1 birdc show protocols all -# Should show 4 peers (peer1, peer2, peer3, peer4) as "Established" -# Each node has N-1 peers (5 nodes = 4 peers per node) - -# Verify all 5 nodes have correct peer counts -for i in {1..5}; do - echo "bird$i:" - docker exec bird$i birdc show protocols | grep -c "Established" -done -# Should show "4" for each node - -# Check TINC mesh -docker exec tinc1 tinc -n bgpmesh info -# Should show connected peers - -# Check etcd cluster -docker exec etcd1 etcdctl endpoint health -# Should show all endpoints healthy - -# Check Prometheus -curl -s http://localhost:9090/-/healthy -# Should return "Prometheus is Healthy." -``` - -### 4. Monitor - -```bash -make monitor -``` - -Opens: -- **Grafana**: http://localhost:3000 (admin/admin) -- **Prometheus**: http://localhost:9090 - -### 5. Run Tests - -```bash -# Run all tests -make test-all - -# Or individual test suites -make test-env # Environment variables check -make test-configs # Configuration template validation -make test-builds # Docker builds -make test-integration # BGP peering, TINC connectivity, etcd propagation -make test-e2e # Full stack workflow with timing -``` - -## Troubleshooting - -### TINC Not Connecting - -```bash -# Check logs -docker logs tinc1 | grep -i error - -# Verify keys generated -docker exec tinc1 ls -la /etc/tinc/bgpmesh/ - -# Check UDP port -docker exec tinc1 netstat -uln | grep 655 - -# Verify interface -docker exec tinc1 ip addr show tinc0 -``` - -### BGP Sessions Flapping - -```bash -# Check session status -docker exec bird1 birdc show protocols all | grep -A 5 peer1 - -# Verify TINC tunnel stable -docker exec tinc1 ping -c 100 10.0.0.2 - -# Check BIRD config -docker exec bird1 cat /etc/bird/bird.conf - -# Review logs -docker logs bird1 | grep -i error -``` - -### etcd Cluster Issues - -```bash -# Check members -docker exec etcd1 etcdctl member list - -# Check status -docker exec etcd1 etcdctl endpoint status --write-out=table - -# Check health -docker exec etcd1 etcdctl endpoint health - -# If issues, recreate cluster -make clean -make deploy-local -``` - -### Container Crashes - -```bash -# Check status -docker ps -a - -# View logs -docker logs bird1 -docker logs tinc1 -docker logs etcd1 - -# Restart individual service -docker restart bird1 - -# Full restart -docker-compose restart -``` - -### Port Conflicts - -If ports 179, 655, 2379, or 9090 are already in use: - -```bash -# Find process using port -sudo lsof -i :179 - -# Kill process or edit docker-compose.yml to use different ports -vim docker-compose.yml -# Change ports section, e.g., "10179:179" for BIRD -``` - -### Slow Convergence - -If deployment takes >2min: - -```bash -# Check system resources -docker stats - -# Check host specs -free -h -df -h - -# If low RAM (<8GB), consider: -# - Closing other applications -# - Building images sequentially instead of parallel -# - Increasing Docker memory limit -``` - -## Development Workflow - -### Modify Configuration - -```bash -# Edit BIRD config template -vim configs/bird/bird.conf.j2 -# Or edit dynamic peer template (Sprint 1.5) -vim configs/bird/protocols.conf.j2 - -# Validate -make test-configs - -# Apply changes (restart containers - all 5 nodes) -docker restart bird1 bird2 bird3 bird4 bird5 - -# Verify -docker exec bird1 birdc show protocols -# Should see 4/4 peers established -``` - -### Modify Docker Image - -```bash -# Edit Dockerfile -vim docker/bird/Dockerfile - -# Rebuild -make clean -make deploy-local - -# Or rebuild single service -docker-compose up -d --build bird1 -``` - -### View Real-time Logs - -```bash -# Follow logs for all services -docker-compose logs -f - -# Follow specific service -docker logs -f bird1 - -# Last 50 lines -docker logs --tail 50 bird1 -``` - -## Teardown - -```bash -# Stop and remove all containers, networks, and volumes -make clean - -# Verify cleanup -docker ps -a | grep bgp -docker volume ls | grep bgp -``` - -## Next Steps - -After successful Sprint 1.5 deployment (5-node full mesh): - -1. **Explore monitoring**: Check Grafana dashboards (http://localhost:3000) -2. **Experiment with configs**: Modify BGP policies in `configs/bird/filters.conf` -3. **Test dynamic scaling**: Add protocols.conf.j2 supports any N nodes -4. **Run chaos tests**: Kill containers and observe reconvergence -5. **Develop Go daemon**: See `daemon-go/README.md` - daemon handles peer propagation -6. **Review manual testing**: See `docs/MANUAL_TESTING.md` for detailed debugging guide -7. **Prepare for Sprint 2**: Review Ansible roles for production deployment - -**Sprint 1.5 Features:** -- ✅ Dynamic BGP peer configuration (N-1 peers auto-generated) -- ✅ TINC layer 2 fix (Subnet declarations for ARP resolution) -- ✅ Pre-commit hooks (gofmt, go vet, tests) - run `./scripts/install-hooks.sh` -- ✅ Scalable full mesh (tested with 5 nodes, 20 BGP sessions, 20 ping paths) - -## Useful Commands - -```bash -# BIRD commands -docker exec bird1 birdc show protocols all -docker exec bird1 birdc show route all -docker exec bird1 birdc show status -docker exec bird1 birdc configure check - -# TINC commands -docker exec tinc1 tinc -n bgpmesh info -docker exec tinc1 tinc -n bgpmesh dump nodes -docker exec tinc1 tinc -n bgpmesh dump edges -docker exec tinc1 tinc -n bgpmesh dump subnets - -# etcd commands -docker exec etcd1 etcdctl get /peers/ --prefix -docker exec etcd1 etcdctl member list -docker exec etcd1 etcdctl endpoint health -docker exec etcd1 etcdctl endpoint status --write-out=table - -# Network debugging -docker exec tinc1 ping -c 3 10.0.0.2 -docker exec bird1 ip route -docker exec bird1 ip addr -``` - -## Support - -- Check [CLAUDE.md](../CLAUDE.md) for development guidelines -- Review [architecture decisions](architecture/decisions.md) -- See [main README](../README.md) for project overview diff --git a/docs/TESTING.md b/docs/TESTING.md deleted file mode 100644 index 1448f44..0000000 --- a/docs/TESTING.md +++ /dev/null @@ -1,253 +0,0 @@ -# Testing Guide - -## Unit Tests - -Run from `daemon-go/`: - -```bash -make test # All tests -make test-unit # Unit only (fast) -make test-race # With race detector -make test-coverage # With coverage report -``` - -### Coverage - -``` -pkg/types: 100% (complete) -pkg/metrics: N/A (no testable statements) -pkg/tinc: 0% (Sprint 2 Phase 2) -pkg/discovery: 0% (Sprint 2 Phase 2) -``` - -Target: >70% overall (currently 85.4% on tested packages) - -### Running Specific Tests - -```bash -go test -v ./pkg/types/ # Single package -go test -v ./... -run TestPeer_String # Single test -go test -v ./... -run ".*Valid.*" # Pattern match -``` - -### Coverage Details - -```bash -make test-coverage # Terminal output -make coverage-html # Generate HTML report -``` - -## Integration Tests - -Prerequisites: - -```bash -cp .env.example .env -make deploy-local -sleep 90 # Wait for convergence -``` - -### BGP Peering Test - -```bash -./tests/integration/test_bgp_peering.sh -``` - -Validates: -- BIRD daemon running on all nodes -- BGP sessions in "Established" state -- Route exchange working - -### TINC Mesh Test - -```bash -./tests/integration/test_tinc_mesh.sh -``` - -Validates: -- tinc0 interface up with correct IPs -- Peer-to-peer connectivity (ping) -- MTU configuration - -### etcd Cluster Test - -```bash -./tests/integration/test_etcd_cluster.sh -``` - -Validates: -- All etcd nodes healthy -- Cluster quorum established -- Read/write operations <10ms - -### Daemon Metrics Test - -```bash -./tests/integration/test_daemon_metrics.sh -``` - -Validates: -- Metrics HTTP server responding on :2112 -- Expected Prometheus metrics exported -- Metric values reasonable - -## CI/CD - -Workflow: `.github/workflows/ci.yml` - -Pipeline: -1. Validate (env, YAML lint) -2. Build (3 Docker images) -3. Test Go (vet, fmt, unit, race, coverage) -4. Integration (deploy + test) - -**Go version**: 1.23 -**Coverage check**: Warns if <70% (not failing in Phase 1) -**Duration**: ~8-12 minutes - -### Local CI Simulation - -```bash -./tests/validation/test_env_vars.sh -./tests/validation/test_configs.sh -make build -cd daemon-go && make ci-test -make deploy-local && sleep 90 && make test-integration -``` - -## Troubleshooting - -### Coverage Below Threshold - -Find uncovered code: - -```bash -cd daemon-go -go test -coverprofile=coverage.out ./... -go tool cover -func=coverage.out | grep -v 100.0% -``` - -Write tests for uncovered functions. - -### Race Detector Failures - -Error: `WARNING: DATA RACE` - -Fix: Use `sync.Mutex`, `sync.RWMutex`, or `atomic` operations - -```bash -go test -race ./... 2>&1 | grep "WARNING: DATA RACE" -``` - -### Docker Tests Hang - -```bash -docker ps -a # Check container status -docker logs # View logs -make clean # Clean environment -make deploy-local # Redeploy -``` - -### Tests Fail After etcd Change - -Issue: Tests depend on clean etcd state - -Fix: - -```bash -docker restart etcd1 etcd2 etcd3 -docker exec etcd1 etcdctl del --prefix / -``` - -## Writing Tests - -### Unit Test Template - -```go -package mypackage - -import ( - "testing" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestMyFunction(t *testing.T) { - tests := []struct { - name string - input string - expected string - wantErr bool - }{ - { - name: "valid input", - input: "test", - expected: "TEST", - wantErr: false, - }, - { - name: "empty input", - input: "", - expected: "", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := MyFunction(tt.input) - - if tt.wantErr { - require.Error(t, err) - return - } - - require.NoError(t, err) - assert.Equal(t, tt.expected, result) - }) - } -} -``` - -### Integration Test Template - -```bash -#!/bin/bash -set -euo pipefail - -echo "=== My Integration Test ===" - -# Test 1 -echo -n "[TEST] Service running... " -if docker exec container1 pgrep myservice >/dev/null; then - echo "[PASS]" -else - echo "[FAIL]" - exit 1 -fi - -# Test 2 -echo -n "[TEST] Endpoint responds... " -if curl -sf http://localhost:8080/health >/dev/null; then - echo "[PASS]" -else - echo "[FAIL]" - exit 1 -fi - -echo "=== All tests passed ===" -``` - -## Test Quality - -Good tests: -- Fast (<1s for unit) -- Isolated (no external dependencies) -- Repeatable (same result every time) -- Readable (clear names) - -Bad tests: -- Slow (>10s for unit) -- Flaky (random failures) -- Coupled (depend on other tests) -- Brittle (break on minor changes) diff --git a/docs/architecture/decisions.md b/docs/architecture/decisions.md deleted file mode 100644 index aa7e6b6..0000000 --- a/docs/architecture/decisions.md +++ /dev/null @@ -1,321 +0,0 @@ -# Architecture Decision Records (ADRs) - -## ADR-001: BIRD 3.x over BIRD 1.6 and FRR - -**Date**: 2025-10-27 -**Status**: Accepted - -### Context - -Need a BGP routing daemon that supports both IPv4 and IPv6 (MP-BGP) for our overlay network. Options considered: -- BIRD 1.6 (legacy, still in use) -- BIRD 3.x (current, MP-BGP unified) -- FRR (feature-rich, Quagga fork) - -### Decision - -Use **BIRD 3.x** (3.1.4+) - -### Rationale - -**Pros:** -- MP-BGP support unified in single daemon (no need for separate bird/bird6) -- Modern config syntax (30-40% less boilerplate vs 1.6) -- RPKI validation built-in (RFC 6811) for route origin validation -- BFD integration for fast reconvergence (<30s) -- Active development and security updates -- Lower memory footprint than FRR (~100MB per 10k routes vs ~200MB) - -**Cons:** -- Higher memory usage than BIRD 1.6 (~20% increase) -- Config syntax incompatible with 1.6 (migration required) -- Fewer production deployments than 1.6 (maturity trade-off) - -**Trade-offs:** -- Memory overhead acceptable given modern hardware targets (>8GB RAM) -- Syntax migration one-time cost, payoff in maintainability -- Faster reconvergence (BFD) worth the ~20MB extra RAM - -### Alternatives Discarded - -- **BIRD 1.6**: EOL, no MP-BGP, legacy syntax -- **FRR**: 2x memory overhead on embedded hardware, overkill for our use case -- **Quagga**: Obsolete (forked to FRR) - -### Consequences - -- Docker image size: ~100MB (acceptable) -- Config templates use BIRD 3.x syntax -- RPKI integration available for future (Sprint 4) -- Reconvergence <30s with BFD (vs ~90s without) - ---- - -## ADR-002: TINC 1.0 over TINC 1.1 and WireGuard - -**Date**: 2025-10-27 -**Status**: Accepted - -### Context - -Need a Layer 2 mesh VPN for BGP overlay. Options: -- TINC 1.0 (legacy, stable, switch mode) -- TINC 1.1 (modern, invitation system) -- WireGuard (fast, point-to-point, kernel-level) - -### Decision - -Use **TINC 1.0** (1.0.36+) - -### Rationale - -**Pros:** -- **Switch mode**: Full Layer 2 mesh, transparent to BGP -- **Legacy compatibility**: Works on OpenWrt 23.05+ (important for future production) -- **Stable**: Battle-tested in production environments -- **NAT traversal**: UDP hole punching for nodes behind firewalls -- **RSA-2048**: Strong encryption, upgrade path to 4096 - -**Cons:** -- Manual key exchange (no invitations like 1.1) -- Older codebase (less active development) -- Higher overhead than WireGuard (~50ms vs ~20ms) - -**Trade-offs:** -- Manual key exchange mitigated by automation (Go daemon in Sprint 2) -- Latency overhead acceptable for dev/test (production tuning later) -- Switch mode essential for L2 BGP adjacency - -### Alternatives Discarded - -- **TINC 1.1**: Invitations nice but incompatible with OpenWrt legacy kernels (<5.10) -- **WireGuard**: Point-to-point only, would need custom mesh logic (complexity) -- **VXLAN**: Requires multicast, not suitable for public internet - -### Consequences - -- Need custom key distribution automation (Sprint 2 Go daemon) -- MTU tuning required (1400 on tun0 vs 1500 on host) -- Compatible with OpenWrt gateways in production -- UDP port 655 must be open in firewalls - ---- - -## ADR-003: etcd over Consul and IPFS - -**Date**: 2025-10-27 -**Status**: Accepted - -### Context - -Need distributed key-value store for: -- Peer propagation (IPs, keys, endpoints) -- Config sync (bird.conf, tinc.conf) -- Health status monitoring - -Options: -- etcd (Raft consensus, watch API) -- Consul (service discovery, DNS) -- IPFS (content-addressable, fully distributed) - -### Decision - -Use **etcd** (v3.5.14+) - -### Rationale - -**Pros:** -- **Lightweight**: 50MB/node vs 200MB Kafka or 500MB+ Consul -- **Raft consensus**: Strong consistency, 3-node quorum tolerates 1 failure -- **Watch API**: Real-time updates for Go daemon -- **Low latency**: <10ms reads for peer lookups -- **Simple ops**: No Zookeeper dependency (unlike Kafka) - -**Cons:** -- Centralized cluster (vs fully distributed IPFS) -- Needs quorum (>50% nodes) for writes -- No built-in service discovery (vs Consul) - -**Trade-offs:** -- Centralized but HA (3-node raft) acceptable for 50-node target scale -- Quorum requirement mitigated by running etcd on stable servers -- Service discovery handled by mDNS in Go daemon (simpler) - -### Alternatives Discarded - -- **Consul**: Heavier, overkill for our needs, DNS features unused -- **IPFS**: Slow cold starts (~30s), high bandwidth overhead (10%), storage issues on OpenWrt -- **Redis**: No consensus, single point of failure without complex Sentinel setup -- **Git**: No real-time updates, not suitable for dynamic state - -### Consequences - -- etcd cluster must be highly available (3+ nodes) -- Quorum loss blocks writes (acceptable for config sync, not critical path) -- Ansible etcd3 module for config management integration -- Encryption at rest needed for secrets (Sprint 3) - ---- - -## ADR-004: Docker Compose over Kubernetes for Sprint 1 - -**Date**: 2025-10-27 (updated 2025-11-03 for Sprint 1.5) -**Status**: Accepted (Sprint 1 & 1.5) - -### Context - -Need local development orchestration for services: -- **Sprint 1**: 9 services (3 bird + 3 tinc + 3 etcd + monitoring) -- **Sprint 1.5**: 21 containers (5 bird + 5 tinc + 5 daemon + 5 etcd + prometheus) - -### Decision - -Use **Docker Compose** for Sprint 1 & 1.5, migrate to **systemd** for production (Sprint 3+) - -### Rationale - -**Pros:** -- **Simplicity**: Single docker-compose.yml, `make deploy-local` converges <2min -- **Low overhead**: No control plane (vs k8s) -- **Local dev optimized**: Fast iteration cycles -- **Multi-stage builds**: Reduce image sizes ~20-30% - -**Cons:** -- Not production-ready (no HA, no auto-scaling) -- Single-host only (no multi-node orchestration) - -**Trade-offs:** -- Perfect for MVP and testing, production uses systemd on Debian/OpenWrt -- Kubernetes overkill for 3-50 node target scale - -### Future Migration - -- Sprint 3: systemd units for production Debian servers -- Sprint 4: OpenWrt native packages (opkg) for gateways - ---- - -## ADR-005: Ansible Push + Pull Hybrid - -**Date**: 2025-10-27 -**Status**: Accepted - -### Context - -Need config management for production deployment and continuous sync. - -### Decision - -Use **Ansible push** for initial provisioning, **ansible-pull** for continuous config management (5min cron) - -### Rationale - -**Pros:** -- **Idempotent**: Safe repeated runs, no config drift -- **Agentless**: SSH-based, works with OpenWrt dropbear -- **ansible-pull**: Mitigates firewall/NAT issues for nodes behind firewalls -- **Fast**: <1min per node for config updates - -**Cons:** -- Not real-time (vs Salt/Puppet agents) -- ansible-pull needs Git repo access - -**Trade-offs:** -- 5min update interval sufficient for config changes (not realtime critical) -- Git dependency acceptable (already using for versioning) - -### Consequences - -- All nodes need Git + Ansible packages -- Secrets management via Ansible Vault (Sprint 3) -- CI/CD integration in Sprint 2 - ---- - -## ADR-006: Go for Custom Daemon over Python - -**Date**: 2025-10-27 -**Status**: Accepted - -### Context - -Need custom daemon for: -- mDNS peer discovery over TINC -- etcd integration (watch /peers/) -- Config sync automation - -### Decision - -Use **Go** (1.21+) - -### Rationale - -**Pros:** -- **Cross-platform**: Single binary for Linux/OpenWrt ARM/x86 -- **Low overhead**: <10MB RAM, <1% CPU idle -- **Concurrency**: Goroutines for etcd watches + mDNS lookup -- **Static binary**: No runtime dependencies (vs Python venv) -- **Performance**: Fast startup (<100ms) - -**Cons:** -- Larger team familiarity with Python -- Compilation step (vs interpreted Python) - -**Trade-offs:** -- Learning curve acceptable given performance benefits -- Static binary deployment simpler than Python deps on OpenWrt - -### Consequences - -- Go 1.21+ required for development -- `go build` produces single binary -- Containerized for dev, native binary for production OpenWrt - ---- - -## ADR-007: TDD Moderate Approach (Opción A) - -**Date**: 2025-10-28 -**Status**: Accepted - -### Context - -Need testing strategy balancing coverage with MVP speed. - -### Decision - -**TDD Moderate** (Opción A): Tests mínimos críticos - config validation, Docker builds, integration, E2E - -**Skip:** Extensive unit tests (Python pytest, Go >80% coverage) -**Focus:** End-to-end functional validation - -### Rationale - -**Pros:** -- **Speed**: ~3-4h implementation (vs 7-8h full TDD) -- **Pragmatic**: Tests critical path without over-engineering -- **Bash-based**: Simple, fast, minimal dependencies - -**Cons:** -- Lower coverage metrics (~60% vs 80%) -- Fewer edge cases caught by unit tests - -**Trade-offs:** -- Sprint 1 MVP prioritizes functional validation -- Unit test expansion in Sprint 2 when daemon matures - -### Consequences - -- `make test-all` validates: env vars, configs, builds, integration, E2E -- CI runs on every push (GitHub Actions) -- Coverage expansion tracked for Sprint 2 - ---- - -## Future ADRs (Planned) - -- **ADR-008**: Route Reflectors for scalability (Sprint 4) -- **ADR-009**: RPKI validation integration (Sprint 4) -- **ADR-010**: Multi-region etcd replication (Sprint 4) -- **ADR-011**: BGP MD5 vs TCP-AO authentication (Sprint 3) -- **ADR-012**: Chaos testing strategy (Sprint 3) diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 6c74a71..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Development Scripts - -This directory contains utility scripts for BGP4mesh development. - -## Available Scripts - -### `install-hooks.sh` - -Installs Git pre-commit hooks for automatic code quality checks. - -**Usage:** -```bash -./scripts/install-hooks.sh -``` - -**What it does:** -- Copies pre-commit hook to `.git/hooks/pre-commit` -- Makes the hook executable -- Configures automatic checks before each commit - -**Pre-commit checks:** -1. Go code formatting (`gofmt -s`) -2. Go static analysis (`go vet`) -3. Unit tests (`make test-unit`) - -**First-time setup:** -```bash -# After cloning the repository -cd BGP4mesh -./scripts/install-hooks.sh -``` - -**Benefits:** -- ✅ Prevents CI failures due to formatting errors -- ✅ Catches issues locally before pushing -- ✅ Ensures consistent code quality -- ✅ Saves time by failing fast - -## Notes - -- Git hooks are not versioned (stored in `.git/hooks/`) -- Each developer needs to run `install-hooks.sh` once after cloning -- Hooks can be bypassed with `git commit --no-verify` (not recommended) - -For more information, see the "Git Hooks & Pre-Commit Checks" section in `CLAUDE.md`. diff --git a/scripts/install-hooks.sh b/scripts/install-hooks.sh deleted file mode 100755 index 709379b..0000000 --- a/scripts/install-hooks.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/bin/bash -# Install git hooks for BGP4mesh project -# This script copies pre-commit hooks to .git/hooks/ - -set -e - -echo "🔧 Installing Git hooks for BGP4mesh..." - -# Get the project root directory -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" -HOOKS_DIR="$PROJECT_ROOT/.git/hooks" - -# Check if we're in a git repository -if [ ! -d "$PROJECT_ROOT/.git" ]; then - echo "❌ Error: Not in a git repository" - echo " Make sure you're running this from the BGP4mesh project directory" - exit 1 -fi - -# Create hooks directory if it doesn't exist -mkdir -p "$HOOKS_DIR" - -# Install pre-commit hook -echo " → Installing pre-commit hook..." -cat > "$HOOKS_DIR/pre-commit" << 'EOF' -#!/bin/bash -# Pre-commit hook for BGP4mesh project -# Prevents commits with formatting issues, vet errors, or failing tests - -set -e - -echo "🔍 Running pre-commit checks..." - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Track if we're in the daemon-go directory -DAEMON_GO_DIR="daemon-go" - -# Check if daemon-go exists (we might be in a subdirectory) -if [ ! -d "$DAEMON_GO_DIR" ]; then - # Try to find it from project root - if [ -d "../daemon-go" ]; then - DAEMON_GO_DIR="../daemon-go" - elif [ -d "../../daemon-go" ]; then - DAEMON_GO_DIR="../../daemon-go" - else - echo -e "${YELLOW}⚠️ daemon-go directory not found, skipping Go checks${NC}" - exit 0 - fi -fi - -# Change to daemon-go directory -cd "$DAEMON_GO_DIR" - -# 1. Check Go formatting -echo -n " → Checking Go formatting... " -UNFORMATTED=$(gofmt -s -l . 2>&1) -if [ -n "$UNFORMATTED" ]; then - echo -e "${RED}✗${NC}" - echo -e "${RED}Error: The following files are not properly formatted:${NC}" - echo "$UNFORMATTED" - echo "" - echo -e "${YELLOW}Fix with: cd daemon-go && gofmt -s -w .${NC}" - exit 1 -fi -echo -e "${GREEN}✓${NC}" - -# 2. Run go vet -echo -n " → Running go vet... " -if ! make vet > /dev/null 2>&1; then - echo -e "${RED}✗${NC}" - echo -e "${RED}Error: go vet found issues${NC}" - make vet - exit 1 -fi -echo -e "${GREEN}✓${NC}" - -# 3. Run unit tests (quick) -echo -n " → Running unit tests... " -if ! make test-unit > /dev/null 2>&1; then - echo -e "${RED}✗${NC}" - echo -e "${RED}Error: Unit tests failed${NC}" - echo "" - echo "Running tests with verbose output:" - make test-unit - exit 1 -fi -echo -e "${GREEN}✓${NC}" - -echo "" -echo -e "${GREEN}✅ All pre-commit checks passed!${NC}" -echo "" - -exit 0 -EOF - -chmod +x "$HOOKS_DIR/pre-commit" - -echo " ✅ Pre-commit hook installed" -echo "" -echo "✨ Git hooks installed successfully!" -echo "" -echo "The pre-commit hook will now run automatically before each commit to:" -echo " • Check Go code formatting (gofmt)" -echo " • Run go vet for code issues" -echo " • Run unit tests" -echo "" -echo "To skip the hook temporarily (not recommended), use:" -echo " git commit --no-verify" -echo "" diff --git a/tests/e2e/test_full_stack.sh b/tests/e2e/test_full_stack.sh deleted file mode 100755 index d084c57..0000000 --- a/tests/e2e/test_full_stack.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "=== E2E: Full Stack Test ===" -START=$(date +%s) - -# Clean any existing deployment -echo "Cleaning previous deployment..." -make clean >/dev/null 2>&1 || true - -# Deploy -echo "Deploying local environment..." -make deploy-local - -# Detect node count -echo "Detecting cluster size..." -sleep 5 # Brief wait for containers to register -NODE_COUNT=$(docker compose ps --services 2>/dev/null | grep -c "^bird" || echo 5) -EXPECTED_PEERS=$((NODE_COUNT - 1)) -echo "✓ Detected $NODE_COUNT nodes" - -# Wait for convergence -echo "Waiting for convergence (90s)..." -sleep 90 - -# Verify BGP on all nodes -echo "Verifying BGP sessions (all $NODE_COUNT nodes)..." -BGP_OK=true -for node_id in $(seq 1 $NODE_COUNT); do - SESSIONS=$(docker exec bird$node_id birdc show protocols 2>/dev/null | grep -c "Established" || echo 0) - if [ "$SESSIONS" -ge "$EXPECTED_PEERS" ]; then - echo " ✓ bird$node_id: $SESSIONS sessions" - else - echo " ✗ bird$node_id: $SESSIONS sessions (expected $EXPECTED_PEERS)" - BGP_OK=false - fi -done - -if [ "$BGP_OK" = true ]; then - echo "✓ All BGP sessions established" -else - echo "✗ Some BGP sessions missing" - exit 1 -fi - -# Verify TINC on all nodes -echo "Verifying TINC mesh (all $NODE_COUNT nodes)..." -TINC_OK=true -for node_id in $(seq 1 $NODE_COUNT); do - if docker exec tinc$node_id ip addr show tinc0 2>/dev/null | grep -q "10.0.0.$node_id"; then - echo " ✓ tinc$node_id: interface up" - else - echo " ✗ tinc$node_id: interface not configured" - TINC_OK=false - fi -done - -if [ "$TINC_OK" = true ]; then - echo "✓ All TINC interfaces up" -else - echo "✗ Some TINC interfaces down" - exit 1 -fi - -# Verify etcd -echo "Verifying etcd cluster..." -if docker exec etcd1 etcdctl endpoint health 2>/dev/null | grep -q "healthy"; then - echo "✓ etcd cluster healthy" -else - echo "✗ etcd cluster unhealthy" - exit 1 -fi - -# Verify monitoring -echo "Verifying Prometheus..." -if curl -sf http://localhost:9090/-/healthy >/dev/null 2>&1; then - echo "✓ Prometheus healthy" -else - echo "✗ Prometheus not accessible" - exit 1 -fi - -# Calculate elapsed time -END=$(date +%s) -ELAPSED=$((END - START)) - -echo "" -echo "=== E2E Test Results ===" -echo "✓ All services operational ($NODE_COUNT nodes)" -echo "✓ BGP peering established ($EXPECTED_PEERS sessions per node)" -echo "✓ TINC mesh connected (full mesh)" -echo "✓ etcd cluster healthy" -echo "✓ Monitoring active" -echo "" -echo "Total time: ${ELAPSED}s (target: <120s)" - -if [ $ELAPSED -lt 120 ]; then - echo "✓ Performance target met" -else - echo "⚠ Convergence slower than target (not critical for dev)" -fi - -echo "" -echo "=== E2E test passed ===" diff --git a/tests/integration/test_bgp_peering.sh b/tests/integration/test_bgp_peering.sh deleted file mode 100755 index 15be75b..0000000 --- a/tests/integration/test_bgp_peering.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "=== Integration Test: BGP Peering ===" - -# Detect node count dynamically from running containers -echo "Detecting cluster size..." -NODE_COUNT=$(docker compose ps --services 2>/dev/null | grep -c "^bird" || echo 0) -if [ "$NODE_COUNT" -eq 0 ]; then - # Fallback: count running bird containers - NODE_COUNT=$(docker ps --format '{{.Names}}' | grep -c "^bird" || echo 0) -fi - -if [ "$NODE_COUNT" -lt 2 ]; then - echo "✗ Insufficient nodes detected (found $NODE_COUNT, need ≥2). Run 'make deploy-local' first." - exit 1 -fi - -EXPECTED_PEERS=$((NODE_COUNT - 1)) # Full mesh: N-1 peers per node -echo "✓ Detected $NODE_COUNT nodes (expecting $EXPECTED_PEERS peers per node)" - -# Wait a bit for BGP to establish -echo "Waiting for BGP convergence (30s)..." -sleep 30 - -# Check BGP sessions on ALL nodes -echo "Testing BGP sessions (full mesh validation)..." -ALL_SESSIONS_OK=true -for node_id in $(seq 1 $NODE_COUNT); do - SESSIONS=$(docker exec bird$node_id birdc show protocols 2>/dev/null | grep -c "peer.*Established" 2>/dev/null || echo "0") - SESSIONS=$(echo "$SESSIONS" | head -n 1 | tr -d '[:space:]') - - if [ "$SESSIONS" -ge "$EXPECTED_PEERS" ] 2>/dev/null; then - echo " ✓ bird$node_id: $SESSIONS/$EXPECTED_PEERS sessions established" - else - echo " ✗ bird$node_id: $SESSIONS/$EXPECTED_PEERS sessions (INSUFFICIENT)" - echo "Debug info for bird$node_id:" - docker exec bird$node_id birdc show protocols || true - ALL_SESSIONS_OK=false - fi -done - -if [ "$ALL_SESSIONS_OK" = true ]; then - echo "✓ All BGP sessions established (full mesh verified)" -else - echo "✗ Some BGP sessions missing (see details above)" - exit 1 -fi - -# Check etcd propagation -echo "Testing etcd propagation..." -PEERS=$(docker exec etcd1 etcdctl get /peers/ --prefix 2>/dev/null | grep -c "node" || echo 0) -echo " Peers in etcd: $PEERS" - -if [ "$PEERS" -ge "$NODE_COUNT" ]; then - echo "✓ etcd propagation working ($PEERS/$NODE_COUNT peers registered)" -else - echo "✗ Insufficient peers in etcd (expected $NODE_COUNT, got $PEERS)" - docker exec etcd1 etcdctl get /peers/ --prefix || true - exit 1 -fi - -# Check TINC interface -echo "Testing TINC interface..." -if docker exec tinc1 ip addr show tinc0 2>/dev/null | grep -q "10.0.0"; then - echo "✓ TINC interface configured" -else - echo "✗ TINC interface not up" - exit 1 -fi - -# Check daemon synced host files (each daemon should have N host files: self + N-1 peers) -echo "Testing daemon host file sync..." -HOST_FILES=$(docker exec daemon1 ls /var/run/tinc/bgpmesh/hosts/ 2>/dev/null | wc -l) -echo " Host files synced: $HOST_FILES" -if [ "$HOST_FILES" -ge "$NODE_COUNT" ]; then - echo "✓ Daemon synced host files ($HOST_FILES files, includes self + peers)" -else - echo "✗ Insufficient host files (expected $NODE_COUNT, got $HOST_FILES)" - docker exec daemon1 ls -la /var/run/tinc/bgpmesh/hosts/ || true - exit 1 -fi - -# Check TINC connections configured via daemon (TINC 1.0 file-based) -echo "Testing TINC dynamic connections..." -TINC_CONNS=$(docker exec tinc1 grep -c "^ConnectTo" /var/run/tinc/bgpmesh/tinc.conf 2>/dev/null || echo 0) -echo " Configured ConnectTo directives: $TINC_CONNS" -if [ "$TINC_CONNS" -ge "$EXPECTED_PEERS" ]; then - echo "✓ TINC full mesh configured ($TINC_CONNS/$EXPECTED_PEERS peers)" -else - echo "✗ Insufficient ConnectTo directives (expected $EXPECTED_PEERS, got $TINC_CONNS)" - echo "Debug: tinc.conf content:" - docker exec tinc1 cat /var/run/tinc/bgpmesh/tinc.conf || true - echo "Debug: Daemon logs (last 30 lines):" - docker logs --tail 30 daemon1 || true - exit 1 -fi - -# Test ping over TINC - Full mesh validation (all pairs) -# Note: Using daemon containers for ping (they share network namespace with tinc) -echo "Testing TINC connectivity (full mesh ping)..." -PING_FAILURES=0 -TOTAL_PINGS=$((NODE_COUNT * EXPECTED_PEERS)) -SUCCESSFUL_PINGS=0 - -for src_id in $(seq 1 $NODE_COUNT); do - for dst_id in $(seq 1 $NODE_COUNT); do - if [ "$src_id" != "$dst_id" ]; then - if docker exec daemon$src_id ping -c 1 -W 2 10.0.0.$dst_id >/dev/null 2>&1; then - SUCCESSFUL_PINGS=$((SUCCESSFUL_PINGS + 1)) - else - echo " ✗ Ping failed: node$src_id → 10.0.0.$dst_id" - PING_FAILURES=$((PING_FAILURES + 1)) - fi - fi - done -done - -if [ "$PING_FAILURES" -eq 0 ]; then - echo "✓ Full mesh TINC connectivity verified ($SUCCESSFUL_PINGS/$TOTAL_PINGS pings successful)" -else - echo "✗ Some pings failed ($PING_FAILURES failures out of $TOTAL_PINGS)" - echo "Debug: tinc.conf content:" - docker exec tinc1 cat /var/run/tinc/bgpmesh/tinc.conf || true - echo "Debug: TINC daemon logs:" - docker logs --tail 20 tinc1 || true - exit 1 -fi - -echo "" -echo "=== Integration tests passed ===" -echo "Summary: $NODE_COUNT nodes, full mesh validated" -echo " - BGP: $EXPECTED_PEERS sessions per node ✓" -echo " - etcd: $NODE_COUNT peers registered ✓" -echo " - TINC: $EXPECTED_PEERS connections per node ✓" -echo " - Connectivity: $TOTAL_PINGS pings successful ✓" diff --git a/tests/validation/test_configs.sh b/tests/validation/test_configs.sh deleted file mode 100755 index c2dc678..0000000 --- a/tests/validation/test_configs.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "=== Validating Configuration Templates ===" - -# Check if jinja2 is available -if ! command -v jinja2 &> /dev/null; then - echo "⚠ jinja2 not found, installing..." - sudo apt-get install -y python3-jinja2 >/dev/null 2>&1 || { - echo "✗ Failed to install jinja2" - exit 1 - } -fi - -# Check if bird is available -if ! command -v bird &> /dev/null; then - echo "⚠ bird not found, skipping syntax validation (will validate in Docker)" - echo "✓ Templates will be validated during Docker build" - exit 0 -fi - -# Render and validate BIRD config -echo "Validating BIRD config..." -if [ -f configs/bird/bird.conf.j2 ]; then - jinja2 configs/bird/bird.conf.j2 \ - -D router_id=192.0.2.1 \ - -D bgp_as=65000 \ - > /tmp/bird_test.conf - - if bird --parse-only -c /tmp/bird_test.conf 2>/dev/null; then - echo "✓ BIRD config valid" - else - echo "✗ BIRD config invalid" - cat /tmp/bird_test.conf - exit 1 - fi - - rm -f /tmp/bird_test.conf -else - echo "⚠ bird.conf.j2 not found yet" -fi - -echo "=== Configuration validation complete ===" diff --git a/tests/validation/test_docker_builds.sh b/tests/validation/test_docker_builds.sh deleted file mode 100755 index dfb3536..0000000 --- a/tests/validation/test_docker_builds.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "=== Testing Docker Builds ===" - -# Build images in parallel -echo "Building bird image..." -docker build ./docker/bird -t bgp-bird:test & -BIRD_PID=$! - -echo "Building tinc image..." -docker build ./docker/tinc -t bgp-tinc:test & -TINC_PID=$! - -echo "Building monitoring image..." -docker build ./docker/monitoring -t bgp-monitoring:test & -MONITORING_PID=$! - -# Wait for all builds -wait $BIRD_PID && echo "✓ BIRD image built" || { echo "✗ BIRD build failed"; exit 1; } -wait $TINC_PID && echo "✓ TINC image built" || { echo "✗ TINC build failed"; exit 1; } -wait $MONITORING_PID && echo "✓ Monitoring image built" || { echo "✗ Monitoring build failed"; exit 1; } - -echo "=== All Docker images built successfully ===" diff --git a/tests/validation/test_env_vars.sh b/tests/validation/test_env_vars.sh deleted file mode 100755 index 3804275..0000000 --- a/tests/validation/test_env_vars.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "=== Validating .env.example ===" - -# Check if .env.example exists -if [ ! -f .env.example ]; then - echo "✗ .env.example not found" - exit 1 -fi - -# Check for required variables -REQUIRED_VARS=("BGP_AS" "TINC_PORT" "ETCD_INITIAL_CLUSTER" "BIRD_PASSWORD" "TINC_NETNAME" "GRAFANA_ADMIN_PASSWORD") - -for var in "${REQUIRED_VARS[@]}"; do - if grep -q "^${var}=" .env.example; then - echo "✓ $var found" - else - echo "✗ $var missing" - exit 1 - fi -done - -echo "=== All required environment variables present ===" diff --git a/tinc_bootstrap.sh b/tinc_bootstrap.sh deleted file mode 100755 index 02dc748..0000000 --- a/tinc_bootstrap.sh +++ /dev/null @@ -1,190 +0,0 @@ -#!/bin/bash -# TINC Mesh Bootstrap Script -# Distributes host files and configures ConnectTo directives for full-mesh connectivity - -set -euo pipefail - -# Configuration -NODES=(1 2 3) -TINC_NETNAME="bgpmesh" -TEMP_DIR=$(mktemp -d) - -# Colors for output -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" -} - -log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -# Cleanup on exit -trap "rm -rf $TEMP_DIR" EXIT - -log_info "TINC Bootstrap Script - Starting" -log_info "Working directory: $TEMP_DIR" -echo "" - -# Step 1: Extract host files from each container -log_info "Step 1: Extracting host files from containers..." - -for i in "${NODES[@]}"; do - container="tinc$i" - host_file="$TEMP_DIR/node$i" - - log_info " Extracting from $container..." - - # Check if host file exists - if docker exec "$container" test -f "/var/run/tinc/$TINC_NETNAME/hosts/node$i"; then - docker cp "$container:/var/run/tinc/$TINC_NETNAME/hosts/node$i" "$host_file" 2>/dev/null || { - log_error "Failed to copy host file from $container" - exit 1 - } - - # Verify file is not empty - if [ ! -s "$host_file" ]; then - log_error "Host file for node$i is empty" - exit 1 - fi - - # Calculate checksum - md5sum "$host_file" >> "$TEMP_DIR/checksums.txt" - log_info " ✓ Extracted node$i host file ($(wc -l < "$host_file") lines)" - else - log_error "Host file not found in $container" - exit 1 - fi -done - -echo "" -log_info "Step 2: Distributing host files to all nodes..." - -# Step 2: Distribute host files cross-node -for i in "${NODES[@]}"; do - container="tinc$i" - - log_info " Configuring $container..." - - for j in "${NODES[@]}"; do - if [[ $i != $j ]]; then - host_file="$TEMP_DIR/node$j" - - # Copy host file to container - docker cp "$host_file" "$container:/var/run/tinc/$TINC_NETNAME/hosts/node$j" 2>/dev/null || { - log_error "Failed to copy node$j to $container" - exit 1 - } - - # Set correct permissions - docker exec "$container" chmod 644 "/var/run/tinc/$TINC_NETNAME/hosts/node$j" || { - log_warn "Could not set permissions for node$j in $container" - } - - log_info " ✓ Added node$j host file" - fi - done -done - -echo "" -log_info "Step 3: Adding ConnectTo directives..." - -# Step 3: Add ConnectTo directives to tinc.conf -for i in "${NODES[@]}"; do - container="tinc$i" - - log_info " Updating $container tinc.conf..." - - # Remove existing ConnectTo lines - docker exec "$container" sed -i '/^ConnectTo/d' "/var/run/tinc/$TINC_NETNAME/tinc.conf" || { - log_warn "Could not remove old ConnectTo in $container" - } - - # Add ConnectTo for each peer - for j in "${NODES[@]}"; do - if [[ $i != $j ]]; then - docker exec "$container" bash -c "echo 'ConnectTo = node$j' >> /var/run/tinc/$TINC_NETNAME/tinc.conf" - log_info " ✓ Added ConnectTo = node$j" - fi - done -done - -echo "" -log_info "Step 4: Reloading TINC daemons..." - -# Step 4: Reload tincd on all nodes -for i in "${NODES[@]}"; do - container="tinc$i" - - log_info " Reloading $container..." - - # Try graceful reload first - if docker exec "$container" tincd -n "$TINC_NETNAME" -kHUP 2>/dev/null; then - log_info " ✓ Graceful reload successful" - else - # Fallback to container restart - log_warn " Graceful reload failed, restarting container..." - docker restart "$container" >/dev/null 2>&1 - log_info " ✓ Container restarted" - fi -done - -# Wait for convergence -log_info "Waiting 15 seconds for mesh convergence..." -sleep 15 - -echo "" -log_info "Step 5: Verifying connectivity..." - -# Step 5: Verify connections -all_connected=true - -for i in "${NODES[@]}"; do - container="tinc$i" - - log_info " Checking $container connections..." - - # Get list of reachable nodes - reachable=$(docker exec "$container" tinc -n "$TINC_NETNAME" dump reachable 2>/dev/null | grep -c "node" || echo "0") - expected=$((${#NODES[@]} - 1)) # Should connect to n-1 peers - - if [ "$reachable" -ge "$expected" ]; then - log_info " ✓ Connected to $reachable peers (expected $expected)" - else - log_warn " ⚠ Only connected to $reachable peers (expected $expected)" - all_connected=false - fi - - # Verify interface is up - if docker exec "$container" ip link show tinc0 2>/dev/null | grep -q "state UP"; then - log_info " ✓ Interface tinc0 is UP" - else - log_error " ✗ Interface tinc0 is DOWN" - all_connected=false - fi -done - -echo "" -echo "======================================" - -if [ "$all_connected" = true ]; then - log_info "TINC mesh bootstrap completed successfully!" - echo "" - log_info "Next steps:" - echo " 1. Verify BGP sessions: docker exec bird1 birdc show protocols" - echo " 2. Test connectivity: docker exec tinc1 ping -c3 10.0.0.2" - echo " 3. Run integration tests: make test-integration" - exit 0 -else - log_error "TINC mesh bootstrap completed with warnings" - log_warn "Check container logs for details: docker logs tinc1" - exit 1 -fi