Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
927e27640a |
@@ -284,7 +284,8 @@ function SpeakerInstallStep({
|
|||||||
<AlertTitle>Сохраните сейчас</AlertTitle>
|
<AlertTitle>Сохраните сейчас</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
agent_secret и node_token больше не будут показаны. Traefik на ноде выпускает
|
agent_secret и node_token больше не будут показаны. Traefik на ноде выпускает
|
||||||
сертификат через DNS-01 (Cloudflare).
|
сертификат через DNS-01 (Cloudflare). MikroTik стучится на IP ноды:179; 80/443 —
|
||||||
|
только агент панели. Логи: docker compose logs -f bird2 evobgp-agent.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Default BIRD 2 config for EvoBGP Docker stack (operator extends with include "bird.d/*.conf";).
|
# Default BIRD 2 config for EvoBGP Docker stack (operator extends with include "bird.d/*.conf";).
|
||||||
router id 192.0.2.1;
|
router id 192.0.2.1;
|
||||||
|
log stderr all;
|
||||||
|
|
||||||
protocol device {
|
protocol device {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,12 @@
|
|||||||
# --env-file .env.remote-speaker --env-file .env.remote-speaker-tls up -d
|
# --env-file .env.remote-speaker --env-file .env.remote-speaker-tls up -d
|
||||||
#
|
#
|
||||||
# Profiles:
|
# Profiles:
|
||||||
# production (default) — bird2 host + agent + evobgp-edge
|
# production (default) — bird2 (speaker-net, 179:179) + agent + evobgp-edge
|
||||||
# plain — bird2 + agent без Traefik (lab)
|
# plain — bird2 + agent без Traefik (lab)
|
||||||
# fallback — + sync-bundle polling
|
# fallback — + sync-bundle polling
|
||||||
|
#
|
||||||
|
# BGP TCP/179 as on the control plane. Overlay sets router id / local.
|
||||||
|
# Logs: docker compose logs -f bird2 evobgp-agent
|
||||||
|
|
||||||
name: evobgp-remote-speaker
|
name: evobgp-remote-speaker
|
||||||
|
|
||||||
@@ -24,13 +27,18 @@ services:
|
|||||||
profiles: ["production", "plain", "fallback"]
|
profiles: ["production", "plain", "fallback"]
|
||||||
image: ${EVOBGP_REGISTRY:-git.shx.one/denozord}/evobgp-bird2:${EVOBGP_IMAGE_TAG:-latest}
|
image: ${EVOBGP_REGISTRY:-git.shx.one/denozord}/evobgp-bird2:${EVOBGP_IMAGE_TAG:-latest}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
# sysctls нельзя с network_mode: host — включите ip_forward на VPS (см. docs/remote-speakers.md)
|
sysctls:
|
||||||
|
net.ipv4.ip_forward: "1"
|
||||||
|
net.ipv6.conf.all.forwarding: "1"
|
||||||
|
ports:
|
||||||
|
- "179:179/tcp"
|
||||||
volumes:
|
volumes:
|
||||||
- bird_etc:/etc/bird
|
- bird_etc:/etc/bird
|
||||||
- bird_run:/run/bird
|
- bird_run:/run/bird
|
||||||
|
networks:
|
||||||
|
- speaker-net
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
||||||
evobgp-agent:
|
evobgp-agent:
|
||||||
|
|||||||
+19
-6
@@ -42,9 +42,11 @@ Runbook для реплик **bird2 + evobgp-agent** на отдельных VPS
|
|||||||
| Порт | Кто | Зачем |
|
| Порт | Кто | Зачем |
|
||||||
|------|-----|-------|
|
|------|-----|-------|
|
||||||
| **443** | IP CP (`PANEL_IP_WHITELIST`) | HTTPS dispatch, health, `GET /v1/agent/bird/protocols` |
|
| **443** | IP CP (`PANEL_IP_WHITELIST`) | HTTPS dispatch, health, `GET /v1/agent/bird/protocols` |
|
||||||
| **179** | BGP peers | Data plane |
|
| **179** | BGP peers | Data plane — Docker `ports: 179:179/tcp`, как на панели |
|
||||||
| **80** | любой | редирект HTTP → HTTPS (не HTTP-01 ACME) |
|
| **80** | любой | редирект HTTP → HTTPS (не HTTP-01 ACME) |
|
||||||
|
|
||||||
|
В панели хостера / security group откройте **TCP 179** (скрипт compose это не делает). Overlay (`bird_bgp_source_ipv4` / `node_ipv4`) задаёт `router id`; host-сеть bird2 не используется.
|
||||||
|
|
||||||
DNS-01 ходит **исходящим** к Cloudflare API и Let's Encrypt; inbound 80 для выпуска сертификата не нужен. Agent слушает `:8443` только во внутренней docker-сети; снаружи — Traefik 443.
|
DNS-01 ходит **исходящим** к Cloudflare API и Let's Encrypt; inbound 80 для выпуска сертификата не нужен. Agent слушает `:8443` только во внутренней docker-сети; снаружи — Traefik 443.
|
||||||
|
|
||||||
Токен Cloudflare для панели (`evobgp-edge` на CP) в процесс API **не проброшен** — для реплики его задают в форме создания.
|
Токен Cloudflare для панели (`evobgp-edge` на CP) в процесс API **не проброшен** — для реплики его задают в форме создания.
|
||||||
@@ -55,15 +57,15 @@ Profile `plain` в файле репозитория — только lab без
|
|||||||
|
|
||||||
| Profile | Состав |
|
| Profile | Состав |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| `production` | bird2 (host) + agent + Traefik LE |
|
| `production` | bird2 (`speaker-net`, `179:179`) + agent + Traefik LE |
|
||||||
| `plain` | bird2 + agent на хосте без Traefik (только lab) |
|
| `plain` | bird2 + agent без Traefik (lab; agent на хосте) |
|
||||||
| `fallback` | + `sync-bundle` polling (`scripts/sync-bundle.sh`) |
|
| `fallback` | + `sync-bundle` polling (`scripts/sync-bundle.sh`) |
|
||||||
|
|
||||||
Команда из UI — самодостаточный yaml **без profiles** (эквивалент production).
|
Команда из UI — самодостаточный yaml **без profiles** (эквивалент production).
|
||||||
|
|
||||||
## Подготовка VPS
|
## Подготовка VPS
|
||||||
|
|
||||||
`bird2` — **`network_mode: host`**. Docker **не может** задать `net.ipv4.ip_forward` в таком контейнере. Команда из UI включает sysctl; для постоянства:
|
`bird2` в docker-сети с `ports: 179:179/tcp` и `sysctls` ip_forward (как панель). Команда из UI дополнительно включает sysctl на хосте:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sysctl -w net.ipv4.ip_forward=1
|
sysctl -w net.ipv4.ip_forward=1
|
||||||
@@ -73,6 +75,18 @@ echo 'net.ipv6.conf.all.forwarding=1' >> /etc/sysctl.d/99-evobgp-bird.conf
|
|||||||
sysctl --system
|
sysctl --system
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Логи на реплике
|
||||||
|
|
||||||
|
BIRD пишет в stderr (`log stderr all`), agent — в stdout. На VPS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/evobgp-speaker
|
||||||
|
docker compose logs -f bird2
|
||||||
|
docker compose logs -f evobgp-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
До первого apply бандла с `protocol bgp` порт 179 может быть CLOSED (нет listener). После sync в логах agent: `sync start` / `sync ok` / `sync failed`.
|
||||||
|
|
||||||
## Безопасность (три участка)
|
## Безопасность (три участка)
|
||||||
|
|
||||||
1. **CP → реплика:** HTTPS (LE) + Traefik ipallowlist + `agent_secret`.
|
1. **CP → реплика:** HTTPS (LE) + Traefik ipallowlist + `agent_secret`.
|
||||||
@@ -105,13 +119,12 @@ Tenant `/v1/settings` (`bird_bgp_source_ipv4`) — fallback для master / ес
|
|||||||
|
|
||||||
| Симптом | Проверка |
|
| Симптом | Проверка |
|
||||||
|---------|----------|
|
|---------|----------|
|
||||||
| `sysctl net.ipv4.ip_forward not allowed in host network` | Уберите sysctls из compose (уже так в main); включите ip_forward на VPS (см. выше) |
|
|
||||||
| `CHANGE_ME_*` в yaml | В форме не заполнены email LE / CF token / IP панели / домен |
|
| `CHANGE_ME_*` в yaml | В форме не заполнены email LE / CF token / IP панели / домен |
|
||||||
| Traefik отдаёт дефолтный сертификат | DNS only; token `Zone:DNS:Edit`; логи `evobgp-edge`; том acme.json |
|
| Traefik отдаёт дефолтный сертификат | DNS only; token `Zone:DNS:Edit`; логи `evobgp-edge`; том acme.json |
|
||||||
| Offline в UI | `GET https://AGENT_DOMAIN/v1/agent/health` с CP; LE cert; whitelist |
|
| Offline в UI | `GET https://AGENT_DOMAIN/v1/agent/health` с CP; LE cert; whitelist |
|
||||||
| dispatch error | CP logs job meta; firewall 443; `agent_secret` |
|
| dispatch error | CP logs job meta; firewall 443; `agent_secret` |
|
||||||
| verify-bundle fail | pubkey совпадает с CP seed; пересоберите pubkey после смены seed |
|
| verify-bundle fail | pubkey совпадает с CP seed; пересоберите pubkey после смены seed |
|
||||||
| BGP не поднимается | bird2 `network_mode: host`; peers; MD5 BGP отдельно от HTTP sync |
|
| BGP не поднимается / сканер CLOSED | `179:179` в compose; SG хостера; `docker compose logs bird2`; пир MikroTik на IP ноды; бандл применён (`sync ok`) |
|
||||||
|
|
||||||
## Ограничения (scale-review)
|
## Ограничения (scale-review)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -117,6 +118,9 @@ func (s *Server) handleSync(w http.ResponseWriter, r *http.Request) {
|
|||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
timeout = 45 * time.Second
|
timeout = 45 * time.Second
|
||||||
}
|
}
|
||||||
|
revID := strings.TrimSpace(req.RevisionID)
|
||||||
|
log.Printf("agentserver: sync start speaker_id=%s revision_id=%q control_plane=%s",
|
||||||
|
strings.TrimSpace(s.cfg.SpeakerID), revID, controlPlaneHost(s.cfg.ControlPlaneURL))
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), timeout)
|
ctx, cancel := context.WithTimeout(r.Context(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -124,7 +128,7 @@ func (s *Server) handleSync(w http.ResponseWriter, r *http.Request) {
|
|||||||
BaseURL: s.cfg.ControlPlaneURL,
|
BaseURL: s.cfg.ControlPlaneURL,
|
||||||
Token: s.cfg.NodeToken,
|
Token: s.cfg.NodeToken,
|
||||||
SpeakerID: s.cfg.SpeakerID,
|
SpeakerID: s.cfg.SpeakerID,
|
||||||
RevisionID: strings.TrimSpace(req.RevisionID),
|
RevisionID: revID,
|
||||||
PubKeyB64: s.cfg.PubKeyB64,
|
PubKeyB64: s.cfg.PubKeyB64,
|
||||||
PubKeyHex: s.cfg.PubKeyHex,
|
PubKeyHex: s.cfg.PubKeyHex,
|
||||||
ExtractDir: s.cfg.ExtractDir,
|
ExtractDir: s.cfg.ExtractDir,
|
||||||
@@ -134,13 +138,16 @@ func (s *Server) handleSync(w http.ResponseWriter, r *http.Request) {
|
|||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("agentserver: sync: %v", err)
|
log.Printf("agentserver: sync failed speaker_id=%s revision_id=%q err=%v",
|
||||||
|
strings.TrimSpace(s.cfg.SpeakerID), revID, err)
|
||||||
writeProblem(w, http.StatusBadGateway, upstreamErrorDetail)
|
writeProblem(w, http.StatusBadGateway, upstreamErrorDetail)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.cfg.OnSyncSuccess != nil {
|
if s.cfg.OnSyncSuccess != nil {
|
||||||
s.cfg.OnSyncSuccess(res.RevisionID)
|
s.cfg.OnSyncSuccess(res.RevisionID)
|
||||||
}
|
}
|
||||||
|
log.Printf("agentserver: sync ok speaker_id=%s applied_revision_id=%s",
|
||||||
|
strings.TrimSpace(s.cfg.SpeakerID), res.RevisionID)
|
||||||
writeJSON(w, http.StatusOK, map[string]any{
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"applied_revision_id": res.RevisionID,
|
"applied_revision_id": res.RevisionID,
|
||||||
@@ -148,6 +155,18 @@ func (s *Server) handleSync(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func controlPlaneHost(raw string) string {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil || strings.TrimSpace(u.Host) == "" {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
return u.Host
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) authorize(r *http.Request) bool {
|
func (s *Server) authorize(r *http.Request) bool {
|
||||||
secret := strings.TrimSpace(s.cfg.Secret)
|
secret := strings.TrimSpace(s.cfg.Secret)
|
||||||
if secret == "" {
|
if secret == "" {
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ func RenderMainBirdConf(opts MainBirdConfOptions) (string, error) {
|
|||||||
}
|
}
|
||||||
b.WriteString("router id ")
|
b.WriteString("router id ")
|
||||||
b.WriteString(strings.TrimSpace(opts.RouterID))
|
b.WriteString(strings.TrimSpace(opts.RouterID))
|
||||||
b.WriteString(";\n\n")
|
b.WriteString(";\n")
|
||||||
|
b.WriteString("log stderr all;\n\n")
|
||||||
for _, inc := range opts.Includes {
|
for _, inc := range opts.Includes {
|
||||||
inc = strings.TrimSpace(inc)
|
inc = strings.TrimSpace(inc)
|
||||||
if inc == "" {
|
if inc == "" {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
router id 192.0.2.1;
|
router id 192.0.2.1;
|
||||||
|
log stderr all;
|
||||||
|
|
||||||
include "bird.d/evobgp_filters_v4.conf";
|
include "bird.d/evobgp_filters_v4.conf";
|
||||||
include "bird.d/evobgp_filters_v6.conf";
|
include "bird.d/evobgp_filters_v6.conf";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# Standard EvoBGP layout: main skeleton + bird.d fragments (matches StandardIncludeFragments).
|
# Standard EvoBGP layout: main skeleton + bird.d fragments (matches StandardIncludeFragments).
|
||||||
|
|
||||||
router id 192.0.2.1;
|
router id 192.0.2.1;
|
||||||
|
log stderr all;
|
||||||
|
|
||||||
include "bird.d/evobgp_filters_v4.conf";
|
include "bird.d/evobgp_filters_v4.conf";
|
||||||
include "bird.d/evobgp_filters_v6.conf";
|
include "bird.d/evobgp_filters_v6.conf";
|
||||||
|
|||||||
@@ -999,9 +999,12 @@ func (s *Server) handleNodeBundle(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
frags := rev.PreviewFragments
|
frags := rev.PreviewFragments
|
||||||
if overlaid, err := pipeline.OverlayFragmentsForSpeaker(s.store, a.TenantID, sid, rid, frags); err == nil {
|
overlaid, err := pipeline.OverlayFragmentsForSpeaker(s.store, a.TenantID, sid, rid, frags)
|
||||||
frags = overlaid
|
if err != nil {
|
||||||
|
writeInternalError(w, "bundle overlay", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
frags = overlaid
|
||||||
tgz, err := bundle.BuildGzippedTar(rid, sid, frags, s.bundlePriv)
|
tgz, err := bundle.BuildGzippedTar(rid, sid, frags, s.bundlePriv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeInternalError(w, "internal", err)
|
writeInternalError(w, "internal", err)
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ func TestPostSpeaker_installCommandsAndMetaObject(t *testing.T) {
|
|||||||
secret,
|
secret,
|
||||||
token,
|
token,
|
||||||
"https://cp.example.com",
|
"https://cp.example.com",
|
||||||
|
`"179:179/tcp"`,
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(cmd, want) {
|
if !strings.Contains(cmd, want) {
|
||||||
t.Errorf("docker_commands missing %q", want)
|
t.Errorf("docker_commands missing %q", want)
|
||||||
|
|||||||
@@ -19,9 +19,13 @@ func BirdLocalsForSpeaker(st store.Backend, tenantID, speakerID string) birdLoca
|
|||||||
return loc
|
return loc
|
||||||
}
|
}
|
||||||
meta := store.ParseSpeakerMeta(sp.MetaJSON)
|
meta := store.ParseSpeakerMeta(sp.MetaJSON)
|
||||||
if s := strings.TrimSpace(meta.BirdBgpSourceIPv4); s != "" {
|
src := strings.TrimSpace(meta.BirdBgpSourceIPv4)
|
||||||
loc.routerID = s
|
if src == "" {
|
||||||
loc.localV4 = s
|
src = strings.TrimSpace(meta.NodeIPv4)
|
||||||
|
}
|
||||||
|
if src != "" {
|
||||||
|
loc.routerID = src
|
||||||
|
loc.localV4 = src
|
||||||
}
|
}
|
||||||
if s := strings.TrimSpace(meta.BirdBgpSourceIPv6); s != "" {
|
if s := strings.TrimSpace(meta.BirdBgpSourceIPv6); s != "" {
|
||||||
loc.localV6 = s
|
loc.localV6 = s
|
||||||
@@ -32,7 +36,7 @@ func BirdLocalsForSpeaker(st store.Backend, tenantID, speakerID string) birdLoca
|
|||||||
// OverlayFragmentsForSpeaker re-renders bird.conf and peers fragment with speaker-specific BIRD locals.
|
// OverlayFragmentsForSpeaker re-renders bird.conf and peers fragment with speaker-specific BIRD locals.
|
||||||
func OverlayFragmentsForSpeaker(st store.Backend, tenantID, speakerID, revisionID string, frags map[string]string) (map[string]string, error) {
|
func OverlayFragmentsForSpeaker(st store.Backend, tenantID, speakerID, revisionID string, frags map[string]string) (map[string]string, error) {
|
||||||
if frags == nil {
|
if frags == nil {
|
||||||
return nil, fmt.Errorf("pipeline: overlay: nil fragments")
|
frags = map[string]string{}
|
||||||
}
|
}
|
||||||
locals := BirdLocalsForSpeaker(st, tenantID, speakerID)
|
locals := BirdLocalsForSpeaker(st, tenantID, speakerID)
|
||||||
out := make(map[string]string, len(frags))
|
out := make(map[string]string, len(frags))
|
||||||
|
|||||||
@@ -40,3 +40,48 @@ func TestOverlayFragmentsForSpeaker_differentRouterID(t *testing.T) {
|
|||||||
t.Fatalf("sp2 router: %s", out2["bird.conf"])
|
t.Fatalf("sp2 router: %s", out2["bird.conf"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOverlayFragmentsForSpeaker_nodeIPv4Fallback(t *testing.T) {
|
||||||
|
m := store.NewMemory()
|
||||||
|
m.SeedDemo()
|
||||||
|
tenant, _, _, _, _ := m.DemoIDs()
|
||||||
|
sp, err := m.CreateSpeaker(tenant, &store.Speaker{
|
||||||
|
Role: "replica",
|
||||||
|
Endpoint: "https://node.example.com",
|
||||||
|
MetaJSON: `{"node_ipv4":"198.51.100.9"}`,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := pipeline.OverlayFragmentsForSpeaker(m, tenant, sp.ID, "rev1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out["bird.conf"], "198.51.100.9") {
|
||||||
|
t.Fatalf("expected node_ipv4 as router id, got: %s", out["bird.conf"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverlayFragmentsForSpeaker_sourceOverridesNodeIPv4(t *testing.T) {
|
||||||
|
m := store.NewMemory()
|
||||||
|
m.SeedDemo()
|
||||||
|
tenant, _, _, _, _ := m.DemoIDs()
|
||||||
|
sp, err := m.CreateSpeaker(tenant, &store.Speaker{
|
||||||
|
Role: "replica",
|
||||||
|
Endpoint: "https://node.example.com",
|
||||||
|
MetaJSON: `{"node_ipv4":"198.51.100.9","bird_bgp_source_ipv4":"203.0.113.40"}`,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
out, err := pipeline.OverlayFragmentsForSpeaker(m, tenant, sp.ID, "rev1", map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(out["bird.conf"], "203.0.113.40") {
|
||||||
|
t.Fatalf("source should win: %s", out["bird.conf"])
|
||||||
|
}
|
||||||
|
if strings.Contains(out["bird.conf"], "198.51.100.9") {
|
||||||
|
t.Fatalf("node_ipv4 should not win over source: %s", out["bird.conf"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type renderData struct {
|
|||||||
|
|
||||||
const composeTemplate = `# EvoBGP replica: bird2 + evobgp-agent + Traefik (Let's Encrypt DNS-01 / Cloudflare).
|
const composeTemplate = `# EvoBGP replica: bird2 + evobgp-agent + Traefik (Let's Encrypt DNS-01 / Cloudflare).
|
||||||
# Generated by control plane. Do not commit secrets. ACME state: volume evobgp_speaker_traefik_letsencrypt.
|
# Generated by control plane. Do not commit secrets. ACME state: volume evobgp_speaker_traefik_letsencrypt.
|
||||||
|
# BGP: ports 179:179 like control plane (overlay sets router id). Logs: docker compose logs -f bird2 evobgp-agent
|
||||||
|
|
||||||
name: evobgp-remote-speaker
|
name: evobgp-remote-speaker
|
||||||
|
|
||||||
@@ -70,12 +71,18 @@ services:
|
|||||||
bird2:
|
bird2:
|
||||||
image: {{.BirdImage}}
|
image: {{.BirdImage}}
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: host
|
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
|
sysctls:
|
||||||
|
net.ipv4.ip_forward: "1"
|
||||||
|
net.ipv6.conf.all.forwarding: "1"
|
||||||
|
ports:
|
||||||
|
- "179:179/tcp"
|
||||||
volumes:
|
volumes:
|
||||||
- bird_etc:/etc/bird
|
- bird_etc:/etc/bird
|
||||||
- bird_run:/run/bird
|
- bird_run:/run/bird
|
||||||
|
networks:
|
||||||
|
- speaker-net
|
||||||
logging: *default-logging
|
logging: *default-logging
|
||||||
|
|
||||||
evobgp-agent:
|
evobgp-agent:
|
||||||
@@ -200,6 +207,8 @@ func dockerCommands(composeYAML string) string {
|
|||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(`# EvoBGP replica: bird2 + agent + Traefik (Let's Encrypt DNS-01 / Cloudflare)
|
b.WriteString(`# EvoBGP replica: bird2 + agent + Traefik (Let's Encrypt DNS-01 / Cloudflare)
|
||||||
# docker login git.shx.one # if images are private
|
# docker login git.shx.one # if images are private
|
||||||
|
# BGP TCP/179 published like the control plane. Cloud security group must allow 179.
|
||||||
|
# Logs: cd /opt/evobgp-speaker && docker compose logs -f bird2 evobgp-agent
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
sysctl -w net.ipv4.ip_forward=1
|
sysctl -w net.ipv4.ip_forward=1
|
||||||
sysctl -w net.ipv6.conf.all.forwarding=1
|
sysctl -w net.ipv6.conf.all.forwarding=1
|
||||||
|
|||||||
@@ -34,11 +34,16 @@ func TestBuild_includesTraefikDNS01(t *testing.T) {
|
|||||||
"docker compose up -d",
|
"docker compose up -d",
|
||||||
"sysctl -w net.ipv4.ip_forward=1",
|
"sysctl -w net.ipv4.ip_forward=1",
|
||||||
"evobgp_speaker_traefik_letsencrypt",
|
"evobgp_speaker_traefik_letsencrypt",
|
||||||
|
`"179:179/tcp"`,
|
||||||
|
"net.ipv4.ip_forward: \"1\"",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(cmd, want) {
|
if !strings.Contains(cmd, want) {
|
||||||
t.Errorf("docker_commands missing %q", want)
|
t.Errorf("docker_commands missing %q", want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if strings.Contains(cmd, "network_mode: host") {
|
||||||
|
t.Error("replica bird2 must not use network_mode: host")
|
||||||
|
}
|
||||||
if strings.Contains(cmd, "?set ") {
|
if strings.Contains(cmd, "?set ") {
|
||||||
t.Error("compose must bake values, not ${VAR:?set VAR}")
|
t.Error("compose must bake values, not ${VAR:?set VAR}")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user