Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b729a3068 | ||
|
|
0b12305600 | ||
|
|
b914b030cc | ||
|
|
bb3987ce1c | ||
|
|
480b2b11e4 | ||
|
|
3302e2c5cd | ||
|
|
3792485c2f | ||
|
|
1a1e095f76 | ||
|
|
11df090913 | ||
|
|
cfb1406201 | ||
|
|
4190c634ec |
@@ -376,3 +376,29 @@ steps:
|
||||
--dockerfile Containerfile \
|
||||
--destination gitea.renner.uno/gitea_admin/netbird-proxy-services:$TAG \
|
||||
--destination gitea.renner.uno/gitea_admin/netbird-proxy-services:latest
|
||||
|
||||
# bird: BGP daemon for the pod-network mesh over pod0 (#576). Runtime
|
||||
# artifact like heating-can; same registry so the audit tracks it.
|
||||
- name: build-bird
|
||||
image: registry.gitlab.com/gitlab-ci-utils/container-images/kaniko:v1.25.18-debug
|
||||
directory: ci/images/bird
|
||||
depends_on: []
|
||||
when:
|
||||
- event: deployment
|
||||
- event: push
|
||||
path: ["ci/images/bird/**", ".woodpecker/build-ci-images.yml"]
|
||||
environment:
|
||||
GITEA_USER: gitea_admin
|
||||
GITEA_TOKEN:
|
||||
from_secret: ci_images_token
|
||||
commands:
|
||||
- mkdir -p /kaniko/.docker
|
||||
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
|
||||
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
|
||||
- TAG=$(date +%Y-%m-%d)
|
||||
- |
|
||||
/kaniko/executor \
|
||||
--context . \
|
||||
--dockerfile Containerfile \
|
||||
--destination gitea.renner.uno/gitea_admin/bird:$TAG \
|
||||
--destination gitea.renner.uno/gitea_admin/bird:latest
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
FROM alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce
|
||||
|
||||
# bird: BGP daemon for the pod-network mesh over pod0. Distro package, no build
|
||||
# step — the image is the binary plus its runtime.
|
||||
#
|
||||
# 3.22 rather than the 3.21 the other images here pin: bird2 first appears in
|
||||
# 3.22/community.
|
||||
#
|
||||
# Config is read from /etc/bird/bird.conf, not the package default
|
||||
# /etc/bird.conf, so the mount cannot shadow what the package ships.
|
||||
#
|
||||
# curl and jq are for the init container, which renders the per-node peer list
|
||||
# from the Kubernetes API. busybox wget cannot verify against the API server's
|
||||
# CA bundle, and the query must not run unverified.
|
||||
RUN apk add --no-cache bird2 curl jq
|
||||
|
||||
# Fails the build rather than the first pod if a binary or its path is wrong.
|
||||
RUN /usr/sbin/bird --version \
|
||||
&& curl --version > /dev/null \
|
||||
&& jq --version > /dev/null
|
||||
|
||||
ENTRYPOINT ["/usr/sbin/bird", "-f", "-c", "/etc/bird/bird.conf"]
|
||||
@@ -32,7 +32,7 @@ spec:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: immich-machine-learning
|
||||
image: ghcr.io/immich-app/immich-machine-learning:v3.2.1 # {"$imagepolicy": "flux-system:immich-machine-learning"}
|
||||
image: ghcr.io/immich-app/immich-machine-learning:v3.2.2 # {"$imagepolicy": "flux-system:immich-machine-learning"}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3003
|
||||
|
||||
@@ -87,7 +87,7 @@ spec:
|
||||
mountPath: /ca
|
||||
containers:
|
||||
- name: immich-server
|
||||
image: ghcr.io/immich-app/immich-server:v3.2.1 # {"$imagepolicy": "flux-system:immich-server"}
|
||||
image: ghcr.io/immich-app/immich-server:v3.2.2 # {"$imagepolicy": "flux-system:immich-server"}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3001
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# Identical on every node, and free of addresses: everything node-specific is
|
||||
# rendered by render-peers.sh from the Kubernetes API.
|
||||
#
|
||||
# defines.conf first, because the templates below reference it; peers.conf last,
|
||||
# because it instantiates them.
|
||||
include "/etc/bird/defines.conf";
|
||||
|
||||
router id from "pod0";
|
||||
log stderr all;
|
||||
|
||||
# Mirrors of the kernel main table, filtered to this node's own pod CIDR.
|
||||
# master4/master6 are implicit and hold what BGP exchanges.
|
||||
ipv4 table kmain4;
|
||||
ipv6 table kmain6;
|
||||
|
||||
protocol device { scan time 10; }
|
||||
|
||||
# Two conditions, both needed. The prefix alone would also match Cilium's
|
||||
# routes for the OTHER nodes, which are protocol kernel as well; the interface
|
||||
# alone would also match cilium_host's own host address. ifname is set for the
|
||||
# gatewayless IPv6 device route too, which is why this works in both families.
|
||||
filter own_pod4 {
|
||||
if net != OWN_POD4 then reject;
|
||||
if ifname !~ "cilium_host" then reject;
|
||||
accept;
|
||||
}
|
||||
filter own_pod6 {
|
||||
if net != OWN_POD6 then reject;
|
||||
if ifname !~ "cilium_host" then reject;
|
||||
accept;
|
||||
}
|
||||
|
||||
# learn all, not learn: plain learn skips routes marked proto kernel, which is
|
||||
# what Cilium's are.
|
||||
protocol kernel klearn4 {
|
||||
learn all;
|
||||
scan time 10;
|
||||
ipv4 { table kmain4; import filter own_pod4; export none; };
|
||||
}
|
||||
protocol kernel klearn6 {
|
||||
learn all;
|
||||
scan time 10;
|
||||
ipv6 { table kmain6; import filter own_pod6; export none; };
|
||||
}
|
||||
|
||||
protocol pipe own4 { table kmain4; peer table master4; export all; import none; }
|
||||
protocol pipe own6 { table kmain6; peer table master6; export all; import none; }
|
||||
|
||||
# Only BGP-learned routes reach table 48. The local CIDR stays in main, where
|
||||
# cilium_host already serves it.
|
||||
filter to_t48 {
|
||||
if source != RTS_BGP then reject;
|
||||
accept;
|
||||
}
|
||||
|
||||
protocol kernel k48v4 {
|
||||
kernel table 48;
|
||||
metric 0;
|
||||
ipv4 { table master4; import none; export filter to_t48; };
|
||||
}
|
||||
protocol kernel k48v6 {
|
||||
kernel table 48;
|
||||
metric 0;
|
||||
ipv6 { table master6; import none; export filter to_t48; };
|
||||
}
|
||||
|
||||
# iBGP over pod0. 64514 keeps clear of Cilium's 64513, which peers with the UDM
|
||||
# from ingress0 and is unrelated.
|
||||
#
|
||||
# No prefix filters: the only route this node offers is its own pod CIDR, which
|
||||
# own_pod4/6 already selected, and the only peers are the other cluster nodes.
|
||||
#
|
||||
# direct is explicit because iBGP defaults to multihop, which makes the gateway
|
||||
# recursive — BIRD would try to resolve the peer in the pod RIB. strict bind
|
||||
# keeps the listener off mgmt0 and ingress0.
|
||||
template bgp pod4 {
|
||||
local as 64514;
|
||||
source address MY4;
|
||||
direct;
|
||||
strict bind;
|
||||
ipv4 { next hop self; import all; export all; };
|
||||
}
|
||||
template bgp pod6 {
|
||||
local as 64514;
|
||||
source address MY6;
|
||||
direct;
|
||||
strict bind;
|
||||
ipv6 { next hop self; import all; export all; };
|
||||
}
|
||||
|
||||
include "/etc/bird/peers.conf";
|
||||
@@ -0,0 +1,102 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: bird
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: bird
|
||||
rules:
|
||||
# The peer list comes from the nodes' pod0 annotations, so no address is
|
||||
# written into this repo twice.
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["get", "list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: bird
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: bird
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: bird
|
||||
namespace: kube-system
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: bird
|
||||
namespace: kube-system
|
||||
labels:
|
||||
tier: node
|
||||
app: bird
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bird
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
tier: node
|
||||
app: bird
|
||||
spec:
|
||||
# Carries pod east-west once the routing rules land. Without it table 48
|
||||
# stays empty and the rules fall through to main, i.e. back to VLAN 40.
|
||||
priorityClassName: system-node-critical
|
||||
# Writes host routes and peers on host addresses.
|
||||
hostNetwork: true
|
||||
serviceAccountName: bird
|
||||
# Every node here is a control plane node, so this is not optional.
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
effect: NoSchedule
|
||||
initContainers:
|
||||
- name: render-peers
|
||||
image: gitea.renner.uno/gitea_admin/bird:2026-09-16
|
||||
# The dated tag was rebuilt today to add curl and jq, so a node may
|
||||
# hold the earlier build of the same tag.
|
||||
imagePullPolicy: Always
|
||||
command: ["/bin/sh", "/config/render-peers.sh"]
|
||||
env:
|
||||
- name: NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /config
|
||||
- name: etc-bird
|
||||
mountPath: /etc/bird
|
||||
containers:
|
||||
- name: bird
|
||||
image: gitea.renner.uno/gitea_admin/bird:2026-09-16
|
||||
imagePullPolicy: Always
|
||||
resources:
|
||||
requests:
|
||||
cpu: "50m"
|
||||
memory: "32Mi"
|
||||
limits:
|
||||
cpu: "200m"
|
||||
memory: "128Mi"
|
||||
securityContext:
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
# NET_ADMIN writes the routes; NET_BIND_SERVICE binds 179.
|
||||
add: ["NET_ADMIN", "NET_BIND_SERVICE"]
|
||||
volumeMounts:
|
||||
- name: etc-bird
|
||||
mountPath: /etc/bird
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: bird-config
|
||||
- name: etc-bird
|
||||
emptyDir: {}
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- daemonset.yaml
|
||||
configMapGenerator:
|
||||
# Hashed name, so editing either file rolls the DaemonSet.
|
||||
- name: bird-config
|
||||
namespace: kube-system
|
||||
files:
|
||||
- bird.conf
|
||||
- render-peers.sh
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/sh
|
||||
# Renders everything node-specific from the Kubernetes API, so no address is
|
||||
# written into this repo twice. pod0 addresses come from annotations set beside
|
||||
# the addresses themselves in kubernetes/talos/patches/k8s-*.yaml; pod CIDRs
|
||||
# come from the Node objects, where Kubernetes assigns them.
|
||||
set -eu
|
||||
|
||||
SA=/var/run/secrets/kubernetes.io/serviceaccount
|
||||
A6=net.renner.uno/pod0-v6
|
||||
A4=net.renner.uno/pod0-v4
|
||||
|
||||
nodes=$(curl -sS --fail \
|
||||
--cacert "$SA/ca.crt" \
|
||||
-H "Authorization: Bearer $(cat "$SA/token")" \
|
||||
https://kubernetes.default.svc/api/v1/nodes)
|
||||
|
||||
# -e throughout, so a node missing an annotation or a pod CIDR fails here,
|
||||
# loudly, rather than forming a mesh with a hole in it.
|
||||
self=$(printf '%s' "$nodes" | jq -er --arg n "$NODE_NAME" \
|
||||
'.items[] | select(.metadata.name == $n)')
|
||||
|
||||
my6=$(printf '%s' "$self" | jq -er --arg a "$A6" '.metadata.annotations[$a]')
|
||||
my4=$(printf '%s' "$self" | jq -er --arg a "$A4" '.metadata.annotations[$a]')
|
||||
|
||||
# podCIDRs is ordered by the cluster's family list, so pick by content rather
|
||||
# than by index.
|
||||
pod6=$(printf '%s' "$self" | jq -er '.spec.podCIDRs[] | select(contains(":"))')
|
||||
pod4=$(printf '%s' "$self" | jq -er '.spec.podCIDRs[] | select(contains(":") | not)')
|
||||
|
||||
cp /config/bird.conf /etc/bird/bird.conf
|
||||
|
||||
cat > /etc/bird/defines.conf <<EOF
|
||||
define MY6 = $my6;
|
||||
define MY4 = $my4;
|
||||
define OWN_POD6 = $pod6;
|
||||
define OWN_POD4 = $pod4;
|
||||
EOF
|
||||
|
||||
# Protocol names are BIRD symbols, so the node name is sanitised.
|
||||
printf '%s' "$nodes" | jq -r \
|
||||
--arg n "$NODE_NAME" --arg a6 "$A6" --arg a4 "$A4" '
|
||||
.items[]
|
||||
| select(.metadata.name != $n)
|
||||
| select(.metadata.annotations[$a6] != null and .metadata.annotations[$a4] != null)
|
||||
| (.metadata.name | gsub("[^a-zA-Z0-9]"; "_")) as $p
|
||||
| "protocol bgp p_\($p)_v6 from pod6 { neighbor \(.metadata.annotations[$a6]) as 64514; }",
|
||||
"protocol bgp p_\($p)_v4 from pod4 { neighbor \(.metadata.annotations[$a4]) as 64514; }"
|
||||
' > /etc/bird/peers.conf
|
||||
|
||||
echo "$NODE_NAME: $(grep -c . /etc/bird/peers.conf) peer protocols, own pod CIDRs $pod6 $pod4"
|
||||
@@ -7,6 +7,7 @@ resources:
|
||||
- cluster-settings/
|
||||
- cilium/
|
||||
- multus/
|
||||
- bird/
|
||||
- cert-manager/
|
||||
- snapshot-controller/
|
||||
- metrics-server/
|
||||
|
||||
@@ -52,6 +52,12 @@ machine:
|
||||
# this label instead of listing hostnames, so which boxes carry storage is
|
||||
# stated once, here, by the machine that knows.
|
||||
storage.renner.uno/ceph-osd: "true"
|
||||
# Publishes this node's pod0 addresses, declared below in its VLANConfig, so
|
||||
# the bird DaemonSet builds its peer list from the node list rather than from
|
||||
# a copy. Annotations, not labels: a label value cannot contain a colon.
|
||||
nodeAnnotations:
|
||||
net.renner.uno/pod0-v6: fd02:48::11
|
||||
net.renner.uno/pod0-v4: 10.2.48.11
|
||||
udev:
|
||||
# Renames every port this node uses to its role at boot, matched by
|
||||
# burned-in MAC — kernel names derive from PCI topology and shift when
|
||||
@@ -182,8 +188,14 @@ addresses:
|
||||
# network's clients selectively, so the client goes rather than the record.
|
||||
# DHCP is off on VLAN 47 for the same reason.
|
||||
#
|
||||
# The metric loses to mgmt0's default 1024 deliberately: this pass only adds
|
||||
# the segment. The default route moves here in the change that flips it.
|
||||
# Both defaults live in table 47, not in main, so only the pod-source rules in
|
||||
# site-homelab.yaml reach them. mgmt0 keeps main's default and with it every
|
||||
# reply the node sends from its own addresses.
|
||||
#
|
||||
# The v6 gateway is the UDM's configured ULA on this segment, the same address
|
||||
# BGP peers with. The RA-learned link-local would be the alternative and is not
|
||||
# a value to write down. accept_ra_defrtr stays 0 for that reason: the RA
|
||||
# default would land in main, where it does not belong.
|
||||
apiVersion: v1alpha1
|
||||
kind: VLANConfig
|
||||
name: ingress0
|
||||
@@ -193,8 +205,10 @@ up: true
|
||||
addresses:
|
||||
- address: 10.2.47.11/24
|
||||
routes:
|
||||
- gateway: fd02:47::1
|
||||
table: "47"
|
||||
- gateway: 10.2.47.1
|
||||
metric: 2048
|
||||
table: "47"
|
||||
---
|
||||
# Dedicated Ceph cluster network on X710 port 1, VLAN 49, untagged on the
|
||||
# wire. OSD-to-OSD replication and nothing else. The udev rule in the machine
|
||||
|
||||
@@ -33,6 +33,10 @@ machine:
|
||||
topology.kubernetes.io/zone: saturn
|
||||
# Holds a Ceph OSD drive — see k8s-1.yaml.
|
||||
storage.renner.uno/ceph-osd: "true"
|
||||
# pod0 addresses for the bird peer list — see k8s-1.yaml.
|
||||
nodeAnnotations:
|
||||
net.renner.uno/pod0-v6: fd02:48::12
|
||||
net.renner.uno/pod0-v4: 10.2.48.12
|
||||
udev:
|
||||
# Renames every port to its role at boot — see k8s-1.yaml.
|
||||
rules:
|
||||
@@ -129,8 +133,10 @@ up: true
|
||||
addresses:
|
||||
- address: 10.2.47.12/24
|
||||
routes:
|
||||
- gateway: fd02:47::1
|
||||
table: "47"
|
||||
- gateway: 10.2.47.1
|
||||
metric: 2048
|
||||
table: "47"
|
||||
---
|
||||
# Dedicated Ceph cluster network on X710 port 1, VLAN 49, untagged on the
|
||||
# wire. OSD-to-OSD replication only, not routed off its own segment.
|
||||
|
||||
@@ -29,6 +29,10 @@ machine:
|
||||
topology.kubernetes.io/zone: neptune
|
||||
# Holds a Ceph OSD drive — see k8s-1.yaml.
|
||||
storage.renner.uno/ceph-osd: "true"
|
||||
# pod0 addresses for the bird peer list — see k8s-1.yaml.
|
||||
nodeAnnotations:
|
||||
net.renner.uno/pod0-v6: fd02:48::13
|
||||
net.renner.uno/pod0-v4: 10.2.48.13
|
||||
udev:
|
||||
# Renames every port to its role at boot — see k8s-1.yaml.
|
||||
rules:
|
||||
@@ -116,8 +120,10 @@ up: true
|
||||
addresses:
|
||||
- address: 10.2.47.13/24
|
||||
routes:
|
||||
- gateway: fd02:47::1
|
||||
table: "47"
|
||||
- gateway: 10.2.47.1
|
||||
metric: 2048
|
||||
table: "47"
|
||||
---
|
||||
# Dedicated Ceph cluster network — see k8s-1.yaml. The node holds NO address
|
||||
# here on purpose; the link is handed whole to the OSD pod (host-device).
|
||||
|
||||
@@ -20,3 +20,47 @@ machine:
|
||||
validSubnets:
|
||||
- 10.2.40.0/24
|
||||
- fd40::/64
|
||||
---
|
||||
# Pod egress leaves over ingress0. Matching on the pod source prefix is what
|
||||
# keeps the node's own traffic out of it: the node IP, the API VIP and apid all
|
||||
# answer on mgmt0, and a reply that left over ingress0 would carry an mgmt0
|
||||
# source onto VLAN 47.
|
||||
#
|
||||
# The routing decision runs before Cilium's masquerade hook, so the rule sees
|
||||
# the pod address and the masquerade then rewrites it to ingress0's.
|
||||
#
|
||||
# Table 47 holds nothing but the two default routes, so the destination rules
|
||||
# below have to run first: without them a pod's packet to a service or to a pod
|
||||
# on another node also matches the source rule, finds only the default, and
|
||||
# leaves for the UDM. Measured — in-cluster DNS stopped resolving.
|
||||
#
|
||||
# They send everything inside the cluster and the local networks back to main.
|
||||
# The prefixes are Cilium's ipv4/ipv6NativeRoutingCIDR, which already mark what
|
||||
# is not foreign traffic.
|
||||
#
|
||||
# Priorities are below main (32766) and clear of the rule Cilium installs at 9.
|
||||
# The two families are separate rule tables, so the priorities need not match;
|
||||
# the document name is the priority and must be unique.
|
||||
apiVersion: v1alpha1
|
||||
kind: RoutingRuleConfig
|
||||
name: "990"
|
||||
dst: fd00::/8
|
||||
table: main
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: RoutingRuleConfig
|
||||
name: "991"
|
||||
dst: 10.0.0.0/8
|
||||
table: main
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: RoutingRuleConfig
|
||||
name: "1000"
|
||||
src: fd10:244::/48
|
||||
table: "47"
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
kind: RoutingRuleConfig
|
||||
name: "1001"
|
||||
src: 10.244.0.0/16
|
||||
table: "47"
|
||||
|
||||
Reference in New Issue
Block a user