Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28ffa18496 |
@@ -1,46 +0,0 @@
|
|||||||
when:
|
|
||||||
- event: push
|
|
||||||
path:
|
|
||||||
include:
|
|
||||||
- mikrotik/coredns/**
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Get registry creds from OpenBao
|
|
||||||
image: quay.io/openbao/openbao:2.5.4
|
|
||||||
environment:
|
|
||||||
VAULT_ADDR: https://openbao.lumpiasty.xyz:8200
|
|
||||||
ROLE_ID:
|
|
||||||
from_secret: renovate_role_id
|
|
||||||
SECRET_ID:
|
|
||||||
from_secret: renovate_secret_id
|
|
||||||
commands:
|
|
||||||
- bao write -field token auth/approle/login
|
|
||||||
role_id=$ROLE_ID
|
|
||||||
secret_id=$SECRET_ID > /woodpecker/.vault_id
|
|
||||||
- export VAULT_TOKEN=$(cat /woodpecker/.vault_id)
|
|
||||||
- 'printf "PLUGIN_USERNAME=%s\n" "$(bao kv get -mount secret -field REGISTRY_USERNAME container-registry)" > /woodpecker/registry.env'
|
|
||||||
- 'printf "PLUGIN_PASSWORD=%s\n" "$(bao kv get -mount secret -field REGISTRY_PASSWORD container-registry)" >> /woodpecker/registry.env'
|
|
||||||
|
|
||||||
- name: Build and push
|
|
||||||
image: woodpeckerci/plugin-docker-buildx:6.1.0
|
|
||||||
privileged: true
|
|
||||||
settings:
|
|
||||||
registry: gitea.lumpiasty.xyz
|
|
||||||
repo: gitea.lumpiasty.xyz/lumpiasty/coredns-mikrotik
|
|
||||||
platforms: linux/arm64
|
|
||||||
tags:
|
|
||||||
- latest
|
|
||||||
- ${CI_COMMIT_SHA:0:8}
|
|
||||||
dockerfile: mikrotik/coredns/Dockerfile
|
|
||||||
context: mikrotik/coredns/
|
|
||||||
env_file: /woodpecker/registry.env
|
|
||||||
|
|
||||||
- name: Invalidate OpenBao token
|
|
||||||
image: quay.io/openbao/openbao:2.5.4
|
|
||||||
environment:
|
|
||||||
VAULT_ADDR: https://openbao.lumpiasty.xyz:8200
|
|
||||||
commands:
|
|
||||||
- export VAULT_TOKEN=$(cat /woodpecker/.vault_id)
|
|
||||||
- bao write -f auth/token/revoke-self
|
|
||||||
when:
|
|
||||||
- status: [success, failure]
|
|
||||||
@@ -21,7 +21,7 @@ steps:
|
|||||||
- bao kv get -mount secret -field RENOVATE_TOKEN renovate > /woodpecker/renovate_token
|
- bao kv get -mount secret -field RENOVATE_TOKEN renovate > /woodpecker/renovate_token
|
||||||
- bao kv get -mount secret -field GITHUB_COM_TOKEN renovate > /woodpecker/github_com_token
|
- bao kv get -mount secret -field GITHUB_COM_TOKEN renovate > /woodpecker/github_com_token
|
||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
image: renovate/renovate:43.220.0
|
image: renovate/renovate:43.207.4
|
||||||
environment:
|
environment:
|
||||||
RENOVATE_AUTODISCOVER: "true"
|
RENOVATE_AUTODISCOVER: "true"
|
||||||
RENOVATE_ENDPOINT: https://gitea.lumpiasty.xyz/api/v1
|
RENOVATE_ENDPOINT: https://gitea.lumpiasty.xyz/api/v1
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ gen-talos-config:
|
|||||||
--config-patch @talos/patches/ollama.patch \
|
--config-patch @talos/patches/ollama.patch \
|
||||||
--config-patch @talos/patches/llama.patch \
|
--config-patch @talos/patches/llama.patch \
|
||||||
--config-patch @talos/patches/frigate.patch \
|
--config-patch @talos/patches/frigate.patch \
|
||||||
--config-patch @talos/patches/woodpecker.patch \
|
|
||||||
--config-patch @talos/patches/anapistula-delrosalae.patch \
|
--config-patch @talos/patches/anapistula-delrosalae.patch \
|
||||||
--output-types controlplane -o talos/generated/anapistula-delrosalae.yaml \
|
--output-types controlplane -o talos/generated/anapistula-delrosalae.yaml \
|
||||||
homelab https://kube-api.homelab.lumpiasty.xyz:6443
|
homelab https://kube-api.homelab.lumpiasty.xyz:6443
|
||||||
|
|||||||
@@ -139,8 +139,6 @@ flowchart TD
|
|||||||
cluster -- "Routes exported via BGP" ----- k8s
|
cluster -- "Routes exported via BGP" ----- k8s
|
||||||
```
|
```
|
||||||
|
|
||||||
More information on network are available in [Network documentation](docs/network.md)
|
|
||||||
|
|
||||||
Currently the k8s cluster consists of single node (hostname anapistula-delrosalae), which is a PC with Ryzen 5 3600, 64GB RAM, RX 580 8GB (for accelerating LLMs), 1TB NVMe SSD, 2TB and 3TB HDDs and serves both as control plane and worker node.
|
Currently the k8s cluster consists of single node (hostname anapistula-delrosalae), which is a PC with Ryzen 5 3600, 64GB RAM, RX 580 8GB (for accelerating LLMs), 1TB NVMe SSD, 2TB and 3TB HDDs and serves both as control plane and worker node.
|
||||||
|
|
||||||
## Software stack
|
## Software stack
|
||||||
@@ -250,8 +248,6 @@ flowchart TD
|
|||||||
| <img src="docs/assets/llama-cpp.svg" alt="LLaMA.cpp" height="50" width="50"> | LLaMA.cpp | LLM inference server running local models with GPU acceleration |
|
| <img src="docs/assets/llama-cpp.svg" alt="LLaMA.cpp" height="50" width="50"> | LLaMA.cpp | LLM inference server running local models with GPU acceleration |
|
||||||
| <img src="docs/assets/llama-swap.svg" alt="llama-swap" height="50" width="50"> | llama-swap | Model swapping for LLaMA.cpp |
|
| <img src="docs/assets/llama-swap.svg" alt="llama-swap" height="50" width="50"> | llama-swap | Model swapping for LLaMA.cpp |
|
||||||
| <img src="docs/assets/meridian.svg" alt="meridian" height="50" width="50"> | Meridian | Proxy that bridges Anthropic's official SDK to third-party tools |
|
| <img src="docs/assets/meridian.svg" alt="meridian" height="50" width="50"> | Meridian | Proxy that bridges Anthropic's official SDK to third-party tools |
|
||||||
| | whisper.cpp | High-performance Whisper Automatic Speech Recognition inference server |
|
|
||||||
| | Kokoro-FastAPI | Kokoro-82M text-to-speech inference server |
|
|
||||||
|
|
||||||
### Applications/Services
|
### Applications/Services
|
||||||
|
|
||||||
@@ -262,7 +258,6 @@ flowchart TD
|
|||||||
| <img src="docs/assets/teamspeak.svg" alt="iSpeak3" height="50" width="50"> | iSpeak3.pl | [ts3server://ispeak3.pl](ts3server://ispeak3.pl) | Public TeamSpeak 3 voice communication server |
|
| <img src="docs/assets/teamspeak.svg" alt="iSpeak3" height="50" width="50"> | iSpeak3.pl | [ts3server://ispeak3.pl](ts3server://ispeak3.pl) | Public TeamSpeak 3 voice communication server |
|
||||||
| <img src="docs/assets/immich.svg" alt="Immich" height="50" width="50"> | Immich | https://immich.lumpiasty.xyz/ | Self-hosted photo and video backup and streaming service |
|
| <img src="docs/assets/immich.svg" alt="Immich" height="50" width="50"> | Immich | https://immich.lumpiasty.xyz/ | Self-hosted photo and video backup and streaming service |
|
||||||
| <img src="docs/assets/frigate.svg" alt="Frigate" height="50" width="50"> | Frigate | https://frigate.lumpiasty.xyz/ | NVR for camera system with AI object detection and classification |
|
| <img src="docs/assets/frigate.svg" alt="Frigate" height="50" width="50"> | Frigate | https://frigate.lumpiasty.xyz/ | NVR for camera system with AI object detection and classification |
|
||||||
| <img src="docs/assets/kaneo.svg" alt="Kaneo" height="50" width="50"> | Kaneo | https://kaneo.lumpiasty.xyz | Project management software |
|
|
||||||
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
@@ -271,7 +266,7 @@ This repo leverages [devenv](https://devenv.sh/) for easy setup of a development
|
|||||||
|
|
||||||
### App deployment
|
### App deployment
|
||||||
|
|
||||||
This repo is being watched by Flux running on cluster. To change config/add new app, simply commit to this repo and wait a while for cluster to reconcile changes. There is a Woodpecker job pushing Flux to reconcile cluster on push to this repository.
|
This repo is being watched by Flux running on cluster. To change config/add new app, simply commit to this repo and wait a while for cluster to reconcile changes. You can speed up this process by "notifying" Flux using `flux reconcile source git flux-system`.
|
||||||
|
|
||||||
Flux watches 3 kustomizations in this repo:
|
Flux watches 3 kustomizations in this repo:
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Secret layout expected in OpenBao (KVv2, mount `secret`):
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `routeros_api` | `username`, `password` |
|
| `routeros_api` | `username`, `password` |
|
||||||
| `wan_pppoe` | `username`, `password` |
|
| `wan_pppoe` | `username`, `password` |
|
||||||
|
| `router_tailscale` | `container_password` |
|
||||||
|
|
||||||
## OpenWrt dlink AP
|
## OpenWrt dlink AP
|
||||||
|
|
||||||
|
|||||||
@@ -39,10 +39,15 @@
|
|||||||
engine_mount_point=openbao_kv_mount
|
engine_mount_point=openbao_kv_mount
|
||||||
).secret[openbao_fields.wan_pppoe.password_key]
|
).secret[openbao_fields.wan_pppoe.password_key]
|
||||||
}}
|
}}
|
||||||
|
routeros_tailscale_container_password: >-
|
||||||
|
{{
|
||||||
|
lookup(
|
||||||
|
'community.hashi_vault.vault_kv2_get',
|
||||||
|
openbao_fields.routeros_tailscale_container.path,
|
||||||
|
engine_mount_point=openbao_kv_mount
|
||||||
|
).secret[openbao_fields.routeros_tailscale_container.container_password_key]
|
||||||
|
}}
|
||||||
no_log: true
|
no_log: true
|
||||||
tags:
|
|
||||||
- tailscale-script
|
|
||||||
|
|
||||||
module_defaults:
|
module_defaults:
|
||||||
group/community.routeros.api:
|
group/community.routeros.api:
|
||||||
|
|||||||
@@ -3,12 +3,9 @@
|
|||||||
community.routeros.api_modify:
|
community.routeros.api_modify:
|
||||||
path: ip address
|
path: ip address
|
||||||
data:
|
data:
|
||||||
- address: 172.20.0.1/24
|
- address: 172.17.0.1/16
|
||||||
interface: containers
|
interface: dockers
|
||||||
network: 172.20.0.0
|
network: 172.17.0.0
|
||||||
- address: 192.168.239.1/30
|
|
||||||
interface: nat64
|
|
||||||
network: 192.168.239.0
|
|
||||||
- address: 192.168.4.1/24
|
- address: 192.168.4.1/24
|
||||||
interface: lo
|
interface: lo
|
||||||
network: 192.168.4.0
|
network: 192.168.4.0
|
||||||
@@ -40,25 +37,19 @@
|
|||||||
community.routeros.api_modify:
|
community.routeros.api_modify:
|
||||||
path: ipv6 address
|
path: ipv6 address
|
||||||
data:
|
data:
|
||||||
- address: fc64::1/126
|
|
||||||
advertise: false
|
|
||||||
comment: nat64 loopback
|
|
||||||
interface: nat64
|
|
||||||
- address: 2001:470:70:dd::2/64
|
- address: 2001:470:70:dd::2/64
|
||||||
advertise: false
|
advertise: false
|
||||||
interface: sit1
|
interface: sit1
|
||||||
# Static instead of from-pool: pool allocation is dynamic (first free /64,
|
- address: ::ffff:ffff:ffff:ffff/64
|
||||||
# e.g. ...:0::/64) which made the RDNSS address advertised in ND config
|
from-pool: pool1
|
||||||
# point at a nonexistent router address. HE prefix is static, so static
|
|
||||||
# per-VLAN addressing is deterministic and matches docs/network.md.
|
|
||||||
- address: 2001:470:61a3:9:ffff:ffff:ffff:ffff/64
|
|
||||||
interface: vlan2
|
interface: vlan2
|
||||||
- address: 2001:470:61a3:500:ffff:ffff:ffff:ffff/64
|
- address: 2001:470:61a3:500:ffff:ffff:ffff:ffff/64
|
||||||
interface: containers
|
interface: dockers
|
||||||
- address: 2001:470:61a3:100::1/64
|
- address: 2001:470:61a3:100::1/64
|
||||||
advertise: false
|
advertise: false
|
||||||
interface: vlan4
|
interface: vlan4
|
||||||
- address: 2001:470:61a3:a:ffff:ffff:ffff:ffff/64
|
- address: ::ffff:ffff:ffff:ffff/64
|
||||||
|
from-pool: pool1
|
||||||
interface: vlan5
|
interface: vlan5
|
||||||
- address: 2001:470:61a3:600::1/64
|
- address: 2001:470:61a3:600::1/64
|
||||||
advertise: false
|
advertise: false
|
||||||
|
|||||||
@@ -5,8 +5,7 @@
|
|||||||
data:
|
data:
|
||||||
- name: bridge1
|
- name: bridge1
|
||||||
vlan-filtering: true
|
vlan-filtering: true
|
||||||
- name: containers
|
- name: dockers
|
||||||
- name: nat64
|
|
||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
@@ -63,15 +62,9 @@
|
|||||||
community.routeros.api_modify:
|
community.routeros.api_modify:
|
||||||
path: interface bridge port
|
path: interface bridge port
|
||||||
data:
|
data:
|
||||||
- bridge: containers
|
- bridge: dockers
|
||||||
interface: veth-tailscale
|
interface: veth1
|
||||||
comment: Tailscale container interface
|
comment: Tailscale container interface
|
||||||
- bridge: containers
|
|
||||||
interface: veth-coredns
|
|
||||||
comment: CoreDNS container interface
|
|
||||||
- bridge: nat64
|
|
||||||
interface: veth-tayga
|
|
||||||
comment: Tayga NAT64 container interface
|
|
||||||
- bridge: bridge1
|
- bridge: bridge1
|
||||||
interface: ether1
|
interface: ether1
|
||||||
pvid: 2
|
pvid: 2
|
||||||
@@ -159,9 +152,24 @@
|
|||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
# Pool is no longer referenced — vlan2/vlan5 now use static addresses
|
- name: Configure DHCP networks
|
||||||
# (addressing.yml) so the RDNSS addresses in ND config are deterministic.
|
community.routeros.api_modify:
|
||||||
# Kept defined for one run after migration; safe to delete afterwards.
|
path: ip dhcp-server network
|
||||||
|
data:
|
||||||
|
- address: 192.168.0.0/24
|
||||||
|
dns-server: 192.168.0.1
|
||||||
|
gateway: 192.168.0.1
|
||||||
|
- address: 192.168.255.0/24
|
||||||
|
dns-none: true
|
||||||
|
gateway: 192.168.255.10
|
||||||
|
- address: 192.168.5.0/24
|
||||||
|
dns-server: 192.168.5.1
|
||||||
|
gateway: 192.168.5.1
|
||||||
|
handle_absent_entries: remove
|
||||||
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
|
# TODO: IPv6 pools are useful when we have dynamic prefix, but we don't
|
||||||
|
# We can remove it now
|
||||||
- name: Configure IPv6 pools
|
- name: Configure IPv6 pools
|
||||||
community.routeros.api_modify:
|
community.routeros.api_modify:
|
||||||
path: ipv6 pool
|
path: ipv6 pool
|
||||||
@@ -180,26 +188,7 @@
|
|||||||
values:
|
values:
|
||||||
allow-remote-requests: true
|
allow-remote-requests: true
|
||||||
cache-size: 20480
|
cache-size: 20480
|
||||||
# CoreDNS container provides DNS64; it forwards upstream to 1.1.1.1/8.8.8.8.
|
servers: 1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001
|
||||||
servers: 172.20.0.3
|
|
||||||
|
|
||||||
- name: Configure DNS static entries
|
|
||||||
community.routeros.api_modify:
|
|
||||||
path: ip dns static
|
|
||||||
data:
|
|
||||||
- name: ts.net
|
|
||||||
type: FWD
|
|
||||||
forward-to: 100.100.100.100
|
|
||||||
match-subdomain: true
|
|
||||||
comment: Tailscale MagicDNS
|
|
||||||
# Do NOT add a lumpiasty.xyz FWD entry here. RouterOS FWD entries return
|
|
||||||
# NOERROR with an empty answer instead of relaying NXDOMAIN, which breaks
|
|
||||||
# getaddrinfo search-domain processing (ENOTFOUND for valid names in k8s
|
|
||||||
# pods). The DNS64 bypass for our own zone lives in the CoreDNS Corefile
|
|
||||||
# (mikrotik/coredns/Corefile, lumpiasty.xyz server block) which relays
|
|
||||||
# rcodes correctly. See docs/coredns-nat64.md pitfall #4.
|
|
||||||
handle_absent_entries: remove
|
|
||||||
handle_entries_content: remove_as_much_as_possible
|
|
||||||
|
|
||||||
- name: Configure NAT-PMP global settings
|
- name: Configure NAT-PMP global settings
|
||||||
community.routeros.api_find_and_modify:
|
community.routeros.api_find_and_modify:
|
||||||
@@ -213,7 +202,7 @@
|
|||||||
community.routeros.api_modify:
|
community.routeros.api_modify:
|
||||||
path: ip nat-pmp interfaces
|
path: ip nat-pmp interfaces
|
||||||
data:
|
data:
|
||||||
- interface: containers
|
- interface: dockers
|
||||||
type: internal
|
type: internal
|
||||||
- interface: pppoe-gpon
|
- interface: pppoe-gpon
|
||||||
type: external
|
type: external
|
||||||
@@ -234,7 +223,7 @@
|
|||||||
community.routeros.api_modify:
|
community.routeros.api_modify:
|
||||||
path: ip upnp interfaces
|
path: ip upnp interfaces
|
||||||
data:
|
data:
|
||||||
- interface: containers
|
- interface: dockers
|
||||||
type: internal
|
type: internal
|
||||||
- interface: pppoe-gpon
|
- interface: pppoe-gpon
|
||||||
type: external
|
type: external
|
||||||
@@ -243,42 +232,6 @@
|
|||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
# Option 108 (IPv6-only preferred, RFC 8925). Without force=yes RouterOS only
|
|
||||||
# includes the option for clients that request code 108 in their Parameter
|
|
||||||
# Request List — i.e. RFC 8925-capable clients. Clients that receive it drop
|
|
||||||
# IPv4 and rely on CLAT/NAT64, which REQUIRES pref64 in RA (see ND tasks below).
|
|
||||||
- name: Configure DHCP server options (IPv6-only preferred, RFC 8925)
|
|
||||||
community.routeros.api_modify:
|
|
||||||
path: ip dhcp-server option
|
|
||||||
data:
|
|
||||||
# 32-bit seconds timer (V6ONLY_WAIT) — how long the client suppresses
|
|
||||||
# IPv4. Refreshed on every renewal; acts as automatic fallback if the
|
|
||||||
# DHCP server disappears. 0x00015180 = 86400 s (1 day).
|
|
||||||
# Quoted to prevent YAML from parsing the hex literal as integer 86400.
|
|
||||||
- name: v6only-preferred
|
|
||||||
code: 108
|
|
||||||
value: "0x00015180"
|
|
||||||
handle_absent_entries: remove
|
|
||||||
handle_entries_content: remove_as_much_as_possible
|
|
||||||
|
|
||||||
- name: Configure DHCP networks
|
|
||||||
community.routeros.api_modify:
|
|
||||||
path: ip dhcp-server network
|
|
||||||
data:
|
|
||||||
- address: 192.168.0.0/24
|
|
||||||
dns-server: 192.168.0.1
|
|
||||||
gateway: 192.168.0.1
|
|
||||||
dhcp-option: v6only-preferred
|
|
||||||
- address: 192.168.255.0/24
|
|
||||||
dns-none: true
|
|
||||||
gateway: 192.168.255.10
|
|
||||||
- address: 192.168.5.0/24
|
|
||||||
dns-server: 192.168.5.1
|
|
||||||
gateway: 192.168.5.1
|
|
||||||
dhcp-option: v6only-preferred
|
|
||||||
handle_absent_entries: remove
|
|
||||||
handle_entries_content: remove_as_much_as_possible
|
|
||||||
|
|
||||||
- name: Configure IPv6 ND defaults
|
- name: Configure IPv6 ND defaults
|
||||||
community.routeros.api_find_and_modify:
|
community.routeros.api_find_and_modify:
|
||||||
ignore_dynamic: false
|
ignore_dynamic: false
|
||||||
@@ -287,30 +240,3 @@
|
|||||||
default: true
|
default: true
|
||||||
values:
|
values:
|
||||||
advertise-dns: true
|
advertise-dns: true
|
||||||
|
|
||||||
# Per-interface ND entries must be CREATED — only the interface=all default
|
|
||||||
# exists out of the box. The previous api_find_and_modify approach silently
|
|
||||||
# matched zero entries and never applied pref64.
|
|
||||||
#
|
|
||||||
# pref64: NAT64 prefix discovery (RFC 8781) — required by clients honouring
|
|
||||||
# DHCP option 108 to activate CLAT. Without it they go IPv6-only with no
|
|
||||||
# working translation and appear stuck while "obtaining IP address".
|
|
||||||
#
|
|
||||||
# dns: RDNSS (RFC 8106) — IPv6-only clients ignore DHCPv4 entirely, including
|
|
||||||
# its dns-server. They need an IPv6 DNS address from RA. We advertise the
|
|
||||||
# router's own per-VLAN IPv6 address; RouterOS DNS forwards to CoreDNS.
|
|
||||||
- name: Configure IPv6 ND per-interface (pref64 + RDNSS)
|
|
||||||
community.routeros.api_modify:
|
|
||||||
path: ipv6 nd
|
|
||||||
data:
|
|
||||||
# advertise-dns must be explicitly enabled — RouterOS creates new ND
|
|
||||||
# entries with advertise-dns=no, which suppresses the RDNSS option
|
|
||||||
# entirely even when a static dns= list is configured.
|
|
||||||
- interface: vlan2
|
|
||||||
advertise-dns: true
|
|
||||||
pref64: 64:ff9b::/96
|
|
||||||
dns: 2001:470:61a3:9:ffff:ffff:ffff:ffff
|
|
||||||
- interface: vlan5
|
|
||||||
advertise-dns: true
|
|
||||||
pref64: 64:ff9b::/96
|
|
||||||
dns: 2001:470:61a3:a:ffff:ffff:ffff:ffff
|
|
||||||
|
|||||||
@@ -5,12 +5,28 @@
|
|||||||
path: container config
|
path: container config
|
||||||
find: {}
|
find: {}
|
||||||
values:
|
values:
|
||||||
tmpdir: tmp
|
registry-url: https://ghcr.io
|
||||||
|
tmpdir: /tmp1/pull
|
||||||
|
|
||||||
- name: Configure container env lists
|
- name: Configure container env lists
|
||||||
community.routeros.api_modify:
|
community.routeros.api_modify:
|
||||||
path: container envs
|
path: container envs
|
||||||
data: []
|
data:
|
||||||
|
- key: ADVERTISE_ROUTES
|
||||||
|
list: tailscale
|
||||||
|
value: 192.168.0.0/24,192.168.1.0/24,192.168.4.1/32,192.168.100.1/32,192.168.255.0/24,10.42.0.0/16,10.43.0.0/16,10.44.0.0/16,2001:470:61a3::/48
|
||||||
|
- key: CONTAINER_GATEWAY
|
||||||
|
list: tailscale
|
||||||
|
value: 172.17.0.1
|
||||||
|
- key: PASSWORD
|
||||||
|
list: tailscale
|
||||||
|
value: "{{ routeros_tailscale_container_password }}"
|
||||||
|
- key: TAILSCALE_ARGS
|
||||||
|
list: tailscale
|
||||||
|
value: --accept-routes --advertise-exit-node --snat-subnet-routes=false
|
||||||
|
- key: UPDATE_TAILSCALE
|
||||||
|
list: tailscale
|
||||||
|
value: y
|
||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
@@ -19,37 +35,29 @@
|
|||||||
path: container mounts
|
path: container mounts
|
||||||
data:
|
data:
|
||||||
- dst: /var/lib/tailscale
|
- dst: /var/lib/tailscale
|
||||||
list: tailscale_state
|
list: tailscale
|
||||||
src: /tailscale/state
|
src: /usb1/tailscale
|
||||||
|
- dst: /root
|
||||||
|
list: tailscale-root
|
||||||
|
src: /tmp1/tailscale-root
|
||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
- name: Configure containers
|
- name: Configure tailscale container
|
||||||
community.routeros.api_modify:
|
community.routeros.api_modify:
|
||||||
path: container
|
path: container
|
||||||
data:
|
data:
|
||||||
- dns: 172.20.0.1
|
- dns: 172.17.0.1
|
||||||
interface: veth-tailscale
|
envlists: tailscale
|
||||||
logging: true
|
hostname: mikrotik
|
||||||
mountlists: tailscale_state
|
interface: veth1
|
||||||
name: tailscale
|
layer-dir: ""
|
||||||
remote-image: gitea.lumpiasty.xyz/lumpiasty/mikrotik-tailscale:stable
|
mountlists: tailscale
|
||||||
root-dir: tailscale/root
|
name: tailscale-mikrotik:latest
|
||||||
|
remote-image: fluent-networks/tailscale-mikrotik:latest
|
||||||
|
root-dir: /usb1/containers/tailscale
|
||||||
start-on-boot: true
|
start-on-boot: true
|
||||||
- dns: 172.20.0.1
|
tmpfs: /tmp:67108864:01777
|
||||||
interface: veth-coredns
|
workdir: /
|
||||||
logging: true
|
|
||||||
name: coredns
|
|
||||||
remote-image: gitea.lumpiasty.xyz/lumpiasty/coredns-mikrotik:latest
|
|
||||||
root-dir: coredns/root
|
|
||||||
start-on-boot: true
|
|
||||||
# Tayga auto-configures from the veth addresses and routes — no env vars needed.
|
|
||||||
- interface: veth-tayga
|
|
||||||
logging: true
|
|
||||||
name: tayga
|
|
||||||
remote-image: ghcr.io/apalrd/tayga-nat64
|
|
||||||
root-dir: tayga/root
|
|
||||||
start-on-boot: true
|
|
||||||
workdir: /app
|
|
||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|||||||
@@ -65,22 +65,8 @@
|
|||||||
out-interface-list: wan
|
out-interface-list: wan
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: forward
|
chain: forward
|
||||||
comment: Allow from containers to everywhere
|
comment: Allow from dockers to everywhere
|
||||||
in-interface: containers
|
in-interface: dockers
|
||||||
- action: accept
|
|
||||||
chain: forward
|
|
||||||
comment: Allow Tayga NAT64 pool to internet
|
|
||||||
out-interface: pppoe-gpon
|
|
||||||
src-address: 192.168.240.0/20
|
|
||||||
# IPv6-only clients reaching internal services published on the public IP
|
|
||||||
# (e.g. authentik.lumpiasty.xyz -> 139.28.40.212 -> dst-nat -> 10.44.0.0/16)
|
|
||||||
# arrive from the Tayga pool after NAT64 translation. Without this rule
|
|
||||||
# they fall through to the final reject (hairpin via NAT64).
|
|
||||||
- action: accept
|
|
||||||
chain: forward
|
|
||||||
comment: Allow Tayga NAT64 pool to LoadBalancer (hairpin port forwards)
|
|
||||||
dst-address: 10.44.0.0/16
|
|
||||||
src-address: 192.168.240.0/20
|
|
||||||
- action: jump
|
- action: jump
|
||||||
chain: forward
|
chain: forward
|
||||||
comment: Allow port forwards
|
comment: Allow port forwards
|
||||||
@@ -151,14 +137,14 @@
|
|||||||
protocol: tcp
|
protocol: tcp
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: input
|
chain: input
|
||||||
comment: Allow DNS from containers
|
comment: Allow DNS from dockers
|
||||||
dst-port: 53
|
dst-port: 53
|
||||||
in-interface: containers
|
in-interface: dockers
|
||||||
protocol: udp
|
protocol: udp
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: input
|
chain: input
|
||||||
dst-port: 53
|
dst-port: 53
|
||||||
in-interface: containers
|
in-interface: dockers
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: input
|
chain: input
|
||||||
@@ -202,9 +188,9 @@
|
|||||||
protocol: udp
|
protocol: udp
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: input
|
chain: input
|
||||||
comment: NAT-PMP from containers (for tailscale)
|
comment: NAT-PMP from dockers (for tailscale)
|
||||||
dst-port: 5351
|
dst-port: 5351
|
||||||
in-interface: containers
|
in-interface: dockers
|
||||||
protocol: udp
|
protocol: udp
|
||||||
- action: reject
|
- action: reject
|
||||||
chain: input
|
chain: input
|
||||||
@@ -243,8 +229,8 @@
|
|||||||
- action: accept
|
- action: accept
|
||||||
chain: allow-ports
|
chain: allow-ports
|
||||||
comment: Allow anything udp to Tailscale
|
comment: Allow anything udp to Tailscale
|
||||||
dst-address: 172.20.0.2
|
dst-address: 172.17.0.2
|
||||||
out-interface: containers
|
out-interface: dockers
|
||||||
protocol: udp
|
protocol: udp
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: allow-ports
|
chain: allow-ports
|
||||||
@@ -268,11 +254,6 @@
|
|||||||
chain: srcnat
|
chain: srcnat
|
||||||
comment: GPON ONT management
|
comment: GPON ONT management
|
||||||
dst-address: 192.168.100.1
|
dst-address: 192.168.100.1
|
||||||
- action: masquerade
|
|
||||||
chain: srcnat
|
|
||||||
comment: Tayga NAT64 dynamic pool to internet
|
|
||||||
out-interface: pppoe-gpon
|
|
||||||
src-address: 192.168.240.0/20
|
|
||||||
- action: dst-nat
|
- action: dst-nat
|
||||||
chain: dstnat
|
chain: dstnat
|
||||||
comment: TS3
|
comment: TS3
|
||||||
@@ -394,30 +375,6 @@
|
|||||||
dst-address: 2001:470:71:dd::/64
|
dst-address: 2001:470:71:dd::/64
|
||||||
out-interface-list: wan
|
out-interface-list: wan
|
||||||
reject-with: icmp-no-route
|
reject-with: icmp-no-route
|
||||||
# Block NAT64-mapped RFC1918 destinations before any broad accept rules.
|
|
||||||
# Without these, NAT64 (64:ff9b::/96) could be used to reach private IPv4
|
|
||||||
# ranges by encoding them in the prefix — bypassing IPv4 forward policy.
|
|
||||||
# 64:ff9b::a00:0/104 = 10.0.0.0/8
|
|
||||||
# 64:ff9b::ac10:0/108 = 172.16.0.0/12
|
|
||||||
# 64:ff9b::c0a8:0/112 = 192.168.0.0/16
|
|
||||||
- action: reject
|
|
||||||
chain: forward
|
|
||||||
comment: Block NAT64 to RFC1918 (10/8)
|
|
||||||
dst-address: 64:ff9b::a00:0/104
|
|
||||||
out-interface: nat64
|
|
||||||
reject-with: icmp-no-route
|
|
||||||
- action: reject
|
|
||||||
chain: forward
|
|
||||||
comment: Block NAT64 to RFC1918 (172.16/12)
|
|
||||||
dst-address: 64:ff9b::ac10:0/108
|
|
||||||
out-interface: nat64
|
|
||||||
reject-with: icmp-no-route
|
|
||||||
- action: reject
|
|
||||||
chain: forward
|
|
||||||
comment: Block NAT64 to RFC1918 (192.168/16)
|
|
||||||
dst-address: 64:ff9b::c0a8:0/112
|
|
||||||
out-interface: nat64
|
|
||||||
reject-with: icmp-no-route
|
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: forward
|
chain: forward
|
||||||
comment: Allow from LAN to everywhere
|
comment: Allow from LAN to everywhere
|
||||||
@@ -455,16 +412,6 @@
|
|||||||
comment: Allow from IOT to internet only
|
comment: Allow from IOT to internet only
|
||||||
in-interface: vlan5
|
in-interface: vlan5
|
||||||
out-interface-list: wan
|
out-interface-list: wan
|
||||||
- action: accept
|
|
||||||
chain: forward
|
|
||||||
comment: Allow from SRV to internet via NAT64
|
|
||||||
in-interface: vlan4
|
|
||||||
out-interface: nat64
|
|
||||||
- action: accept
|
|
||||||
chain: forward
|
|
||||||
comment: Allow from IOT to internet via NAT64
|
|
||||||
in-interface: vlan5
|
|
||||||
out-interface: nat64
|
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: forward
|
chain: forward
|
||||||
comment: Allow from OPENWRT UPLINK to internet only
|
comment: Allow from OPENWRT UPLINK to internet only
|
||||||
@@ -472,17 +419,14 @@
|
|||||||
out-interface-list: wan
|
out-interface-list: wan
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: forward
|
chain: forward
|
||||||
comment: Allow from containers to everywhere
|
comment: Allow from dockers to everywhere
|
||||||
in-interface: containers
|
in-interface: dockers
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: forward
|
chain: forward
|
||||||
comment: Allow from internet to containers
|
comment: Allow from internet to dockers
|
||||||
dst-address: 2001:470:61a3:500::/64
|
dst-address: 2001:470:61a3:500::/64
|
||||||
in-interface-list: wan
|
in-interface-list: wan
|
||||||
out-interface: containers
|
out-interface: dockers
|
||||||
# NAT64 to Tayga is now covered by the broad per-VLAN accept rules above.
|
|
||||||
# RFC1918-mapped destinations are blocked at the top of the chain before
|
|
||||||
# those broad accepts, so no separate per-source NAT64 rules are needed.
|
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: forward
|
chain: forward
|
||||||
comment: Allow tcp transmission port to LAN
|
comment: Allow tcp transmission port to LAN
|
||||||
@@ -541,14 +485,14 @@
|
|||||||
protocol: tcp
|
protocol: tcp
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: input
|
chain: input
|
||||||
comment: Allow DNS from containers
|
comment: Allow DNS from dockers
|
||||||
dst-port: 53
|
dst-port: 53
|
||||||
in-interface: containers
|
in-interface: dockers
|
||||||
protocol: udp
|
protocol: udp
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: input
|
chain: input
|
||||||
dst-port: 53
|
dst-port: 53
|
||||||
in-interface: containers
|
in-interface: dockers
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
- action: accept
|
- action: accept
|
||||||
chain: input
|
chain: input
|
||||||
|
|||||||
@@ -39,43 +39,19 @@
|
|||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item.default_name }}"
|
label: "{{ item.default_name }}"
|
||||||
|
|
||||||
# community.routeros.api_modify can't remove hardware disks
|
|
||||||
# but it tries to do so with handle_absent_entries: remove
|
|
||||||
# Working around by manually deleting other ones
|
|
||||||
|
|
||||||
- name: Read current disk entries
|
|
||||||
community.routeros.api_info:
|
|
||||||
path: disk
|
|
||||||
register: routeros_disks
|
|
||||||
check_mode: false
|
|
||||||
|
|
||||||
- name: Remove stale software-defined disk entries
|
|
||||||
community.routeros.api:
|
|
||||||
path: disk
|
|
||||||
remove: "{{ item['.id'] }}"
|
|
||||||
loop: >-
|
|
||||||
{{
|
|
||||||
routeros_disks.result
|
|
||||||
| rejectattr('type', 'in', ['hardware', 'partition'])
|
|
||||||
| rejectattr('slot', 'equalto', 'tmp')
|
|
||||||
}}
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.slot }}"
|
|
||||||
|
|
||||||
- name: Create temporary disk for containers if absent
|
|
||||||
community.routeros.api:
|
|
||||||
path: disk
|
|
||||||
add: "slot=tmp type=tmpfs"
|
|
||||||
when: routeros_disks.result | selectattr('slot', 'equalto', 'tmp') | list | length == 0
|
|
||||||
|
|
||||||
- name: Configure temporary disk for containers
|
- name: Configure temporary disk for containers
|
||||||
community.routeros.api_find_and_modify:
|
community.routeros.api_modify:
|
||||||
ignore_dynamic: false
|
|
||||||
path: disk
|
path: disk
|
||||||
find:
|
data:
|
||||||
slot: tmp
|
- slot: tmp1
|
||||||
values:
|
type: tmpfs
|
||||||
type: tmpfs
|
# This is not ideal, there's no unique identifier for usb disk,
|
||||||
|
# after reinstall it might be assigned to another slot
|
||||||
|
# Just adding disk with slot usb1 and not specifying anything else
|
||||||
|
# so ansible doesn't touch it
|
||||||
|
- slot: usb1
|
||||||
|
handle_absent_entries: remove
|
||||||
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
- name: Configure switch settings
|
- name: Configure switch settings
|
||||||
community.routeros.api_find_and_modify:
|
community.routeros.api_find_and_modify:
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
- name: Preflight checks
|
- name: Preflight checks
|
||||||
ansible.builtin.import_tasks: preflight.yml
|
ansible.builtin.import_tasks: preflight.yml
|
||||||
|
|
||||||
- name: WAN and tunnel interfaces
|
|
||||||
ansible.builtin.import_tasks: wan.yml
|
|
||||||
|
|
||||||
- name: Base network configuration
|
- name: Base network configuration
|
||||||
ansible.builtin.import_tasks: base.yml
|
ansible.builtin.import_tasks: base.yml
|
||||||
|
|
||||||
|
- name: WAN and tunnel interfaces
|
||||||
|
ansible.builtin.import_tasks: wan.yml
|
||||||
|
|
||||||
- name: Hardware and platform tuning
|
- name: Hardware and platform tuning
|
||||||
ansible.builtin.import_tasks: hardware.yml
|
ansible.builtin.import_tasks: hardware.yml
|
||||||
|
|
||||||
|
|||||||
@@ -32,4 +32,15 @@
|
|||||||
fail_msg: "RouterOS device-mode does not report container as enabled. Payload: {{ routeros_device_mode | to_nice_json }}"
|
fail_msg: "RouterOS device-mode does not report container as enabled. Payload: {{ routeros_device_mode | to_nice_json }}"
|
||||||
success_msg: "RouterOS device-mode confirms container=yes"
|
success_msg: "RouterOS device-mode confirms container=yes"
|
||||||
|
|
||||||
|
- name: Read configured disks
|
||||||
|
community.routeros.api_info:
|
||||||
|
path: disk
|
||||||
|
register: routeros_disks
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Assert usb1 disk is present
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- (routeros_disks.result | selectattr('slot', 'equalto', 'usb1') | list | length) > 0
|
||||||
|
fail_msg: "Required disk slot usb1 is not present on router."
|
||||||
|
success_msg: "Required disk usb1 is present"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
disabled: false
|
disabled: false
|
||||||
distance: 1
|
distance: 1
|
||||||
dst-address: 100.64.0.0/10
|
dst-address: 100.64.0.0/10
|
||||||
gateway: 172.20.0.2
|
gateway: 172.17.0.2
|
||||||
routing-table: main
|
routing-table: main
|
||||||
scope: 30
|
scope: 30
|
||||||
suppress-hw-offload: false
|
suppress-hw-offload: false
|
||||||
@@ -21,15 +21,6 @@
|
|||||||
suppress-hw-offload: false
|
suppress-hw-offload: false
|
||||||
target-scope: 10
|
target-scope: 10
|
||||||
vrf-interface: pppoe-gpon
|
vrf-interface: pppoe-gpon
|
||||||
- comment: Tayga NAT64 dynamic pool
|
|
||||||
disabled: false
|
|
||||||
distance: 1
|
|
||||||
dst-address: 192.168.240.0/20
|
|
||||||
gateway: 192.168.239.2
|
|
||||||
routing-table: main
|
|
||||||
scope: 30
|
|
||||||
suppress-hw-offload: false
|
|
||||||
target-scope: 10
|
|
||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
@@ -50,13 +41,6 @@
|
|||||||
pref-src: ""
|
pref-src: ""
|
||||||
routing-table: main
|
routing-table: main
|
||||||
suppress-hw-offload: false
|
suppress-hw-offload: false
|
||||||
- comment: NAT64 prefix via Tayga
|
|
||||||
disabled: false
|
|
||||||
distance: 1
|
|
||||||
dst-address: 64:ff9b::/96
|
|
||||||
gateway: fc64::2%nat64
|
|
||||||
routing-table: main
|
|
||||||
suppress-hw-offload: false
|
|
||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
|
|||||||
@@ -19,101 +19,6 @@
|
|||||||
handle_absent_entries: ignore
|
handle_absent_entries: ignore
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|
||||||
# The RouterOS API can neither store multi-line script source (newlines
|
|
||||||
# collapse into one line) nor evaluate the [/file/get ...] expression itself.
|
|
||||||
# So we fetch the update logic as a .rsc file onto the router's flash, then run
|
|
||||||
# a single-line bootstrap script (which the API CAN store) whose body RouterOS
|
|
||||||
# evaluates natively: it builds the real, browsable, multi-line named script
|
|
||||||
# from the file via [/file get ... contents]. The scheduler then runs that
|
|
||||||
# named script by name (the upstream-intended design). The update logic stays
|
|
||||||
# out of this repo entirely.
|
|
||||||
- name: Download tailscale auto-update script to router
|
|
||||||
community.routeros.api:
|
|
||||||
path: tool
|
|
||||||
cmd: >-
|
|
||||||
fetch
|
|
||||||
url=https://gitea.lumpiasty.xyz/Lumpiasty/mikrotik-tailscale/raw/branch/main/routeros/update-tailscale.rsc
|
|
||||||
dst-path=update-tailscale.rsc
|
|
||||||
mode=https
|
|
||||||
changed_when: true
|
|
||||||
tags:
|
|
||||||
- tailscale-script
|
|
||||||
|
|
||||||
- name: Build the named auto-update script from the fetched file
|
|
||||||
community.routeros.api:
|
|
||||||
path: system script
|
|
||||||
cmd: >-
|
|
||||||
add name=update-tailscale-bootstrap
|
|
||||||
source=":do { /system script remove update-tailscale } on-error={};
|
|
||||||
/system script add name=update-tailscale
|
|
||||||
comment=\"Check for mikrotik-tailscale image updates\"
|
|
||||||
source=[/file get update-tailscale.rsc contents]"
|
|
||||||
changed_when: true
|
|
||||||
tags:
|
|
||||||
- tailscale-script
|
|
||||||
|
|
||||||
- name: Find bootstrap script id
|
|
||||||
community.routeros.api:
|
|
||||||
path: system script
|
|
||||||
extended_query:
|
|
||||||
attributes: [.id, name]
|
|
||||||
where:
|
|
||||||
- attribute: name
|
|
||||||
is: "=="
|
|
||||||
value: update-tailscale-bootstrap
|
|
||||||
register: routeros_bootstrap
|
|
||||||
changed_when: false
|
|
||||||
tags:
|
|
||||||
- tailscale-script
|
|
||||||
|
|
||||||
- name: Run bootstrap to create the named auto-update script
|
|
||||||
community.routeros.api:
|
|
||||||
path: system script
|
|
||||||
cmd: "run .id={{ routeros_bootstrap.msg[0]['.id'] }}"
|
|
||||||
register: routeros_bootstrap_run
|
|
||||||
failed_when:
|
|
||||||
- routeros_bootstrap_run is failed
|
|
||||||
- "'interrupted' not in (routeros_bootstrap_run.msg | string)"
|
|
||||||
changed_when: true
|
|
||||||
tags:
|
|
||||||
- tailscale-script
|
|
||||||
|
|
||||||
- name: Verify named auto-update script exists
|
|
||||||
community.routeros.api:
|
|
||||||
path: system script
|
|
||||||
extended_query:
|
|
||||||
attributes: [.id, name]
|
|
||||||
where:
|
|
||||||
- attribute: name
|
|
||||||
is: "=="
|
|
||||||
value: update-tailscale
|
|
||||||
register: routeros_named_script
|
|
||||||
failed_when: (routeros_named_script.msg | length) == 0
|
|
||||||
changed_when: false
|
|
||||||
tags:
|
|
||||||
- tailscale-script
|
|
||||||
|
|
||||||
- name: Remove bootstrap script
|
|
||||||
community.routeros.api:
|
|
||||||
path: system script
|
|
||||||
remove: "{{ routeros_bootstrap.msg[0]['.id'] }}"
|
|
||||||
changed_when: true
|
|
||||||
tags:
|
|
||||||
- tailscale-script
|
|
||||||
|
|
||||||
- name: Configure tailscale auto-update scheduler
|
|
||||||
community.routeros.api_modify:
|
|
||||||
path: system scheduler
|
|
||||||
data:
|
|
||||||
- name: update-tailscale
|
|
||||||
interval: 1d
|
|
||||||
on-event: /system script run update-tailscale
|
|
||||||
comment: Check for mikrotik-tailscale image updates
|
|
||||||
handle_absent_entries: remove
|
|
||||||
handle_entries_content: remove_as_much_as_possible
|
|
||||||
tags:
|
|
||||||
- tailscale-script
|
|
||||||
|
|
||||||
- name: Configure service ports and service enablement
|
- name: Configure service ports and service enablement
|
||||||
community.routeros.api_find_and_modify:
|
community.routeros.api_find_and_modify:
|
||||||
ignore_dynamic: false
|
ignore_dynamic: false
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
keepalive-timeout: 2
|
keepalive-timeout: 2
|
||||||
name: pppoe-gpon
|
name: pppoe-gpon
|
||||||
password: "{{ routeros_pppoe_password }}"
|
password: "{{ routeros_pppoe_password }}"
|
||||||
# Using CoreDNS container with DNS64
|
use-peer-dns: true
|
||||||
use-peer-dns: false
|
|
||||||
user: "{{ routeros_pppoe_username }}"
|
user: "{{ routeros_pppoe_username }}"
|
||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
@@ -30,24 +29,13 @@
|
|||||||
community.routeros.api_modify:
|
community.routeros.api_modify:
|
||||||
path: interface veth
|
path: interface veth
|
||||||
data:
|
data:
|
||||||
- address: 172.20.0.2/24,2001:470:61a3:500::1/64
|
- address: 172.17.0.2/16,2001:470:61a3:500::1/64
|
||||||
container-mac-address: 7E:7E:A1:B1:2A:7C
|
container-mac-address: 7E:7E:A1:B1:2A:7C
|
||||||
dhcp: false
|
dhcp: false
|
||||||
gateway: 172.20.0.1
|
gateway: 172.17.0.1
|
||||||
gateway6: 2001:470:61a3:500:ffff:ffff:ffff:ffff
|
gateway6: 2001:470:61a3:500:ffff:ffff:ffff:ffff
|
||||||
mac-address: 7E:7E:A1:B1:2A:7B
|
mac-address: 7E:7E:A1:B1:2A:7B
|
||||||
name: veth-tailscale
|
name: veth1
|
||||||
comment: Tailscale container
|
comment: Tailscale container
|
||||||
- address: 172.20.0.3/24
|
|
||||||
dhcp: false
|
|
||||||
gateway: 172.20.0.1
|
|
||||||
name: veth-coredns
|
|
||||||
comment: CoreDNS container
|
|
||||||
- address: 192.168.239.2/30,fc64::2/126
|
|
||||||
dhcp: false
|
|
||||||
gateway: 192.168.239.1
|
|
||||||
gateway6: fc64::1
|
|
||||||
name: veth-tayga
|
|
||||||
comment: Tayga NAT64 container
|
|
||||||
handle_absent_entries: remove
|
handle_absent_entries: remove
|
||||||
handle_entries_content: remove_as_much_as_possible
|
handle_entries_content: remove_as_much_as_possible
|
||||||
|
|||||||
@@ -14,4 +14,6 @@ openbao_fields:
|
|||||||
path: wan_pppoe
|
path: wan_pppoe
|
||||||
username_key: username
|
username_key: username
|
||||||
password_key: password
|
password_key: password
|
||||||
|
routeros_tailscale_container:
|
||||||
|
path: router_tailscale
|
||||||
|
container_password_key: container_password
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ spec:
|
|||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: authentik
|
chart: authentik
|
||||||
version: 2026.5.3
|
version: 2026.5.2
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: HelmRepository
|
kind: HelmRepository
|
||||||
name: authentik
|
name: authentik
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: caddy
|
- name: caddy
|
||||||
image: caddy:2.11.4-alpine
|
image: caddy:2.11.3-alpine
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- mountPath: /etc/caddy
|
- mountPath: /etc/caddy
|
||||||
|
|||||||
+27
-199
@@ -14,13 +14,12 @@ macros:
|
|||||||
qwen35_35b_heretic_mmproj: "--mmproj-url https://huggingface.co/unsloth/Qwen3.5-35B-A3B-GGUF/resolve/main/mmproj-F16.gguf --mmproj /root/.cache/llama.cpp/unsloth_Qwen3.5-35B-A3B-GGUF_mmproj-F16.gguf"
|
qwen35_35b_heretic_mmproj: "--mmproj-url https://huggingface.co/unsloth/Qwen3.5-35B-A3B-GGUF/resolve/main/mmproj-F16.gguf --mmproj /root/.cache/llama.cpp/unsloth_Qwen3.5-35B-A3B-GGUF_mmproj-F16.gguf"
|
||||||
qwen35_4b_heretic_mmproj: "--mmproj-url https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/mmproj-F16.gguf --mmproj /root/.cache/llama.cpp/unsloth_Qwen3.5-4B-GGUF_mmproj-F16.gguf"
|
qwen35_4b_heretic_mmproj: "--mmproj-url https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/mmproj-F16.gguf --mmproj /root/.cache/llama.cpp/unsloth_Qwen3.5-4B-GGUF_mmproj-F16.gguf"
|
||||||
gemma4_sampling: "--temp 1.0 --top-p 0.95 --top-k 64 -ctk q4_0 -ctv q4_0"
|
gemma4_sampling: "--temp 1.0 --top-p 0.95 --top-k 64 -ctk q4_0 -ctv q4_0"
|
||||||
gemma4_nothink_sampling: "--temp 1.0 --top-p 0.95 --top-k 64 -ctk q4_0 -ctv q4_0 --reasoning off"
|
|
||||||
|
|
||||||
hooks:
|
hooks:
|
||||||
on_startup:
|
on_startup:
|
||||||
preload:
|
preload:
|
||||||
- "Qwen3.5-0.8B-GGUF-nothink:Q4_K_XL"
|
- "Qwen3.5-0.8B-GGUF-nothink:Q4_K_XL"
|
||||||
- "parakeet-tdt_ctc-1.1b"
|
- "whisper-small"
|
||||||
|
|
||||||
# matrix replaces groups (they are mutually exclusive).
|
# matrix replaces groups (they are mutually exclusive).
|
||||||
# The small 0.8B model runs alongside any LLM.
|
# The small 0.8B model runs alongside any LLM.
|
||||||
@@ -28,7 +27,7 @@ hooks:
|
|||||||
matrix:
|
matrix:
|
||||||
vars:
|
vars:
|
||||||
q8: "Qwen3.5-0.8B-GGUF-nothink:Q4_K_XL"
|
q8: "Qwen3.5-0.8B-GGUF-nothink:Q4_K_XL"
|
||||||
stt: "parakeet-tdt_ctc-1.1b"
|
stt: "whisper-small"
|
||||||
flux: "flux2-klein-4b:Q4_K_M"
|
flux: "flux2-klein-4b:Q4_K_M"
|
||||||
coder: "Qwen3-Coder-Next-GGUF:Q4_K_M"
|
coder: "Qwen3-Coder-Next-GGUF:Q4_K_M"
|
||||||
q35t: "Qwen3.5-35B-A3B-GGUF:Q4_K_M"
|
q35t: "Qwen3.5-35B-A3B-GGUF:Q4_K_M"
|
||||||
@@ -39,24 +38,10 @@ matrix:
|
|||||||
q4nt: "Qwen3.5-4B-GGUF-nothink:Q4_K_M"
|
q4nt: "Qwen3.5-4B-GGUF-nothink:Q4_K_M"
|
||||||
q4ht: "Qwen3.5-4B-heretic-GGUF:Q4_K_M"
|
q4ht: "Qwen3.5-4B-heretic-GGUF:Q4_K_M"
|
||||||
q4hnt: "Qwen3.5-4B-heretic-GGUF-nothink:Q4_K_M"
|
q4hnt: "Qwen3.5-4B-heretic-GGUF-nothink:Q4_K_M"
|
||||||
g26xl: "gemma-4-26B-A4B-it-qat:UD-Q4_K_XL"
|
g26xl: "gemma-4-26B-A4B-it:UD-Q4_K_XL"
|
||||||
g26xlnt: "gemma-4-26B-A4B-it-qat-nothink:UD-Q4_K_XL"
|
g26q2: "gemma-4-26B-A4B-it:UD-Q2_K_XL"
|
||||||
g26mtp: "gemma-4-26B-A4B-it-qat-mtp:UD-Q4_K_XL"
|
ge4xl: "unsloth/gemma-4-E4B-it-GGUF:UD-Q4_K_XL"
|
||||||
g26mtpnt: "gemma-4-26B-A4B-it-qat-mtp-nothink:UD-Q4_K_XL"
|
ge2xl: "unsloth/gemma-4-E2B-it-GGUF:UD-Q4_K_XL"
|
||||||
g26ht: "SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF:UD-Q4_K_XL"
|
|
||||||
g26hnt: "SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF-nothink:UD-Q4_K_XL"
|
|
||||||
g26hmtp: "SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF-mtp:UD-Q4_K_XL"
|
|
||||||
g26hmnt: "SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF-mtp-nothink:UD-Q4_K_XL"
|
|
||||||
ge4qat: "unsloth/gemma-4-E4B-it-qat-GGUF:UD-Q4_K_XL"
|
|
||||||
ge4qatnt: "unsloth/gemma-4-E4B-it-qat-GGUF-nothink:UD-Q4_K_XL"
|
|
||||||
ge2qat: "unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL"
|
|
||||||
ge2qatnt: "unsloth/gemma-4-E2B-it-qat-GGUF-nothink:UD-Q4_K_XL"
|
|
||||||
ge4mtp: "unsloth/gemma-4-E4B-it-qat-GGUF-mtp:UD-Q4_K_XL"
|
|
||||||
ge4mtpnt: "unsloth/gemma-4-E4B-it-qat-GGUF-mtp-nothink:UD-Q4_K_XL"
|
|
||||||
ge4ht: "llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF:Q4_K_M"
|
|
||||||
ge4hnt: "llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF-nothink:Q4_K_M"
|
|
||||||
ge4hmtp: "llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF-mtp:Q4_K_M"
|
|
||||||
ge4hmnt: "llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF-mtp-nothink:Q4_K_M"
|
|
||||||
q36t: "unsloth/Qwen3.6-35B-A3B-GGUF:UD-Q4_K_XL"
|
q36t: "unsloth/Qwen3.6-35B-A3B-GGUF:UD-Q4_K_XL"
|
||||||
q36nt: "unsloth/Qwen3.6-35B-A3B-GGUF-nothink:UD-Q4_K_XL"
|
q36nt: "unsloth/Qwen3.6-35B-A3B-GGUF-nothink:UD-Q4_K_XL"
|
||||||
haut: "HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive:Q4_K_M"
|
haut: "HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive:Q4_K_M"
|
||||||
@@ -69,11 +54,10 @@ matrix:
|
|||||||
|
|
||||||
sets:
|
sets:
|
||||||
# any LLM can run alongside the small always-on model + STT + TTS (all CPU, no VRAM cost)
|
# any LLM can run alongside the small always-on model + STT + TTS (all CPU, no VRAM cost)
|
||||||
with_q8: "(coder | q35t | q35nt | q35ht | q35hnt | q4t | q4nt | q4ht | q4hnt | g26xl | g26xlnt | g26mtp | g26mtpnt | g26ht | g26hnt | g26hmtp | g26hmnt | ge4qat | ge4qatnt | ge2qat | ge2qatnt | ge4mtp | ge4mtpnt | ge4ht | ge4hnt | ge4hmtp | ge4hmnt | q36t | q36nt | haut | haunt | mtpt | mtpnt) & q8 & stt"
|
with_q8: "(coder | q35t | q35nt | q35ht | q35hnt | q4t | q4nt | q4ht | q4hnt | g26xl | g26q2 | ge4xl | ge2xl | q36t | q36nt | haut | haunt | mtpt | mtpnt) & q8 & stt"
|
||||||
# FLUX runs alone — evicts everything including q8, but keeps STT for voice during image gen
|
# FLUX runs alone — evicts everything including q8, but keeps STT for voice during image gen
|
||||||
image_gen: "flux & stt"
|
image_gen: "flux & stt"
|
||||||
|
|
||||||
|
|
||||||
models:
|
models:
|
||||||
"Qwen3-Coder-Next-GGUF:Q4_K_M":
|
"Qwen3-Coder-Next-GGUF:Q4_K_M":
|
||||||
cmd: |
|
cmd: |
|
||||||
@@ -167,200 +151,38 @@ models:
|
|||||||
${qwen35_nothink_args}
|
${qwen35_nothink_args}
|
||||||
${common_args}
|
${common_args}
|
||||||
|
|
||||||
"gemma-4-26B-A4B-it-qat:UD-Q4_K_XL":
|
"gemma-4-26B-A4B-it:UD-Q4_K_XL":
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server
|
llama-server
|
||||||
-hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:UD-Q4_K_XL \
|
-hf unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL \
|
||||||
${ctx_256k}
|
${ctx_256k}
|
||||||
${gemma4_sampling}
|
${gemma4_sampling}
|
||||||
${common_args}
|
${common_args}
|
||||||
|
|
||||||
"gemma-4-26B-A4B-it-qat-nothink:UD-Q4_K_XL":
|
"gemma-4-26B-A4B-it:UD-Q2_K_XL":
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server
|
llama-server
|
||||||
-hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:UD-Q4_K_XL \
|
-hf unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q2_K_XL \
|
||||||
${ctx_256k}
|
|
||||||
${gemma4_nothink_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"gemma-4-26B-A4B-it-qat-mtp:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:UD-Q4_K_XL \
|
|
||||||
--spec-draft-hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:Q8_0-MTP \
|
|
||||||
--spec-type draft-mtp
|
|
||||||
--spec-draft-n-max 1
|
|
||||||
--swa-full
|
|
||||||
--kv-unified
|
|
||||||
--parallel 1
|
|
||||||
${ctx_256k}
|
${ctx_256k}
|
||||||
${gemma4_sampling}
|
${gemma4_sampling}
|
||||||
${common_args}
|
${common_args}
|
||||||
|
|
||||||
"gemma-4-26B-A4B-it-qat-mtp-nothink:UD-Q4_K_XL":
|
"unsloth/gemma-4-E4B-it-GGUF:UD-Q4_K_XL":
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server
|
llama-server
|
||||||
-hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:UD-Q4_K_XL \
|
-hf unsloth/gemma-4-E4B-it-GGUF:UD-Q4_K_XL \
|
||||||
--spec-draft-hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:Q8_0-MTP \
|
|
||||||
--spec-type draft-mtp
|
|
||||||
--spec-draft-n-max 1
|
|
||||||
--swa-full
|
|
||||||
--kv-unified
|
|
||||||
--parallel 1
|
|
||||||
${ctx_256k}
|
|
||||||
${gemma4_nothink_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF:UD-Q4_K_XL \
|
|
||||||
${ctx_256k}
|
|
||||||
${gemma4_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF-nothink:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF:UD-Q4_K_XL \
|
|
||||||
${ctx_256k}
|
|
||||||
${gemma4_nothink_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
# The heretic QAT repo does not ship an MTP drafter,
|
|
||||||
# so borrow the one from the non-heretic unsloth QAT repo.
|
|
||||||
"SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF-mtp:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF:UD-Q4_K_XL \
|
|
||||||
--spec-draft-hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:Q8_0-MTP \
|
|
||||||
--spec-type draft-mtp
|
|
||||||
--spec-draft-n-max 1
|
|
||||||
--swa-full
|
|
||||||
--kv-unified
|
|
||||||
--parallel 1
|
|
||||||
${ctx_256k}
|
|
||||||
${gemma4_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF-mtp-nothink:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf SC117/gemma-4-26B-A4B-it-qat-heretic-GGUF:UD-Q4_K_XL \
|
|
||||||
--spec-draft-hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:Q8_0-MTP \
|
|
||||||
--spec-type draft-mtp
|
|
||||||
--spec-draft-n-max 1
|
|
||||||
--swa-full
|
|
||||||
--kv-unified
|
|
||||||
--parallel 1
|
|
||||||
${ctx_256k}
|
|
||||||
${gemma4_nothink_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"unsloth/gemma-4-E4B-it-qat-GGUF:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf unsloth/gemma-4-E4B-it-qat-GGUF:UD-Q4_K_XL \
|
|
||||||
${ctx_128k}
|
${ctx_128k}
|
||||||
${gemma4_sampling}
|
${gemma4_sampling}
|
||||||
${common_args}
|
${common_args}
|
||||||
|
|
||||||
"unsloth/gemma-4-E4B-it-qat-GGUF-nothink:UD-Q4_K_XL":
|
"unsloth/gemma-4-E2B-it-GGUF:UD-Q4_K_XL":
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server
|
llama-server
|
||||||
-hf unsloth/gemma-4-E4B-it-qat-GGUF:UD-Q4_K_XL \
|
-hf unsloth/gemma-4-E2B-it-GGUF:UD-Q4_K_XL \
|
||||||
${ctx_128k}
|
|
||||||
${gemma4_nothink_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL \
|
|
||||||
${ctx_128k}
|
${ctx_128k}
|
||||||
${gemma4_sampling}
|
${gemma4_sampling}
|
||||||
${common_args}
|
${common_args}
|
||||||
|
|
||||||
"unsloth/gemma-4-E2B-it-qat-GGUF-nothink:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf unsloth/gemma-4-E2B-it-qat-GGUF:UD-Q4_K_XL \
|
|
||||||
${ctx_128k}
|
|
||||||
${gemma4_nothink_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"unsloth/gemma-4-E4B-it-qat-GGUF-mtp:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf unsloth/gemma-4-E4B-it-qat-GGUF:UD-Q4_K_XL \
|
|
||||||
--spec-draft-hf unsloth/gemma-4-E4B-it-qat-GGUF:Q8_0-MTP \
|
|
||||||
--spec-type draft-mtp
|
|
||||||
--spec-draft-n-max 1
|
|
||||||
--swa-full
|
|
||||||
--kv-unified
|
|
||||||
--parallel 1
|
|
||||||
${ctx_128k}
|
|
||||||
${gemma4_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"unsloth/gemma-4-E4B-it-qat-GGUF-mtp-nothink:UD-Q4_K_XL":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf unsloth/gemma-4-E4B-it-qat-GGUF:UD-Q4_K_XL \
|
|
||||||
--spec-draft-hf unsloth/gemma-4-E4B-it-qat-GGUF:Q8_0-MTP \
|
|
||||||
--spec-type draft-mtp
|
|
||||||
--spec-draft-n-max 1
|
|
||||||
--swa-full
|
|
||||||
--kv-unified
|
|
||||||
--parallel 1
|
|
||||||
${ctx_128k}
|
|
||||||
${gemma4_nothink_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF:Q4_K_M":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF:Q4_K_M \
|
|
||||||
${ctx_128k}
|
|
||||||
${gemma4_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF-nothink:Q4_K_M":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF:Q4_K_M \
|
|
||||||
${ctx_128k}
|
|
||||||
${gemma4_nothink_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF-mtp:Q4_K_M":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF:Q4_K_M \
|
|
||||||
--spec-draft-hf unsloth/gemma-4-E4B-it-qat-GGUF:Q8_0-MTP \
|
|
||||||
--spec-type draft-mtp
|
|
||||||
--spec-draft-n-max 1
|
|
||||||
--swa-full
|
|
||||||
--kv-unified
|
|
||||||
--parallel 1
|
|
||||||
${ctx_128k}
|
|
||||||
${gemma4_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF-mtp-nothink:Q4_K_M":
|
|
||||||
cmd: |
|
|
||||||
llama-server
|
|
||||||
-hf llmfan46/gemma-4-E4B-it-ultra-uncensored-heretic-GGUF:Q4_K_M \
|
|
||||||
--spec-draft-hf unsloth/gemma-4-E4B-it-qat-GGUF:Q8_0-MTP \
|
|
||||||
--spec-type draft-mtp
|
|
||||||
--spec-draft-n-max 1
|
|
||||||
--swa-full
|
|
||||||
--kv-unified
|
|
||||||
--parallel 1
|
|
||||||
${ctx_128k}
|
|
||||||
${gemma4_nothink_sampling}
|
|
||||||
${common_args}
|
|
||||||
|
|
||||||
"unsloth/Qwen3.6-35B-A3B-GGUF:UD-Q4_K_XL":
|
"unsloth/Qwen3.6-35B-A3B-GGUF:UD-Q4_K_XL":
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server
|
llama-server
|
||||||
@@ -413,14 +235,20 @@ models:
|
|||||||
--parallel 1
|
--parallel 1
|
||||||
${common_args}
|
${common_args}
|
||||||
|
|
||||||
# STT via parakeet-server (parakeet.cpp OpenAI-compatible server, CPU, always loaded)
|
# STT via whisper.cpp (Vulkan GPU on RX 580, always loaded, ~600MB VRAM)
|
||||||
# Model downloaded on first start and cached under /root/.cache/parakeet.cpp/models
|
# Model auto-downloaded by init container, see deployment.yaml
|
||||||
# parakeet-proxy.py sits in front to convert any audio format to WAV via ffmpeg,
|
# Note: Vulkan whisper on AMD GPUs has known quality issues on some cards;
|
||||||
# since parakeet-server only accepts real WAV but browsers send Ogg/Opus.
|
# if transcriptions come out as garbage/gibberish, add --no-gpu to fall back.
|
||||||
"parakeet-tdt_ctc-1.1b":
|
"whisper-small":
|
||||||
checkEndpoint: none
|
checkEndpoint: none
|
||||||
cmd: |
|
cmd: |
|
||||||
env PROXY_PORT=${PORT} FFMPEG_BIN=/root/.cache/ffmpeg/ffmpeg python3 /config/parakeet-proxy.py
|
whisper-server
|
||||||
|
--port ${PORT}
|
||||||
|
-m /root/.cache/whisper/ggml-small.bin
|
||||||
|
--request-path /v1/audio
|
||||||
|
--inference-path /transcriptions
|
||||||
|
--convert
|
||||||
|
--threads 6
|
||||||
|
|
||||||
|
|
||||||
# Image generation via stable-diffusion.cpp (sd-server)
|
# Image generation via stable-diffusion.cpp (sd-server)
|
||||||
|
|||||||
@@ -1,227 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Thin reverse proxy for parakeet-server.
|
|
||||||
|
|
||||||
Accepts POST /v1/audio/transcriptions with any audio format,
|
|
||||||
converts the audio to 16 kHz mono WAV via ffmpeg, then forwards
|
|
||||||
the converted file to the real parakeet-server running on PARAKEET_PORT.
|
|
||||||
|
|
||||||
Also proxies GET /health straight through.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
PROXY_PORT=<port> PARAKEET_PORT=<upstream> python3 parakeet-proxy.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import http.server
|
|
||||||
import io
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import urllib.request
|
|
||||||
import urllib.error
|
|
||||||
|
|
||||||
PROXY_PORT = int(os.environ.get("PROXY_PORT", "8080"))
|
|
||||||
PARAKEET_PORT = PROXY_PORT + 1
|
|
||||||
FFMPEG = os.environ.get("FFMPEG_BIN", "ffmpeg")
|
|
||||||
MODEL = os.environ.get("PARAKEET_MODEL", "tdt_ctc-1.1b-q4_k.gguf")
|
|
||||||
CACHE_DIR = os.environ.get("PARAKEET_CACHE_DIR", "/root/.cache/parakeet.cpp/models")
|
|
||||||
|
|
||||||
|
|
||||||
def convert_to_wav(data: bytes) -> bytes:
|
|
||||||
"""Convert any audio bytes to 16 kHz mono PCM WAV via ffmpeg."""
|
|
||||||
with tempfile.NamedTemporaryFile(suffix=".input", delete=False) as inf:
|
|
||||||
inf.write(data)
|
|
||||||
inf_path = inf.name
|
|
||||||
out_path = inf_path + ".wav"
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
FFMPEG, "-y",
|
|
||||||
"-i", inf_path,
|
|
||||||
"-ar", "16000",
|
|
||||||
"-ac", "1",
|
|
||||||
"-f", "wav",
|
|
||||||
out_path,
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
stdout=subprocess.DEVNULL,
|
|
||||||
stderr=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
with open(out_path, "rb") as f:
|
|
||||||
return f.read()
|
|
||||||
finally:
|
|
||||||
os.unlink(inf_path)
|
|
||||||
if os.path.exists(out_path):
|
|
||||||
os.unlink(out_path)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_multipart(content_type: str, body: bytes):
|
|
||||||
"""
|
|
||||||
Parse a multipart/form-data body.
|
|
||||||
Returns a dict of field_name -> (filename_or_None, content_type, data).
|
|
||||||
"""
|
|
||||||
import email
|
|
||||||
from email import policy as email_policy
|
|
||||||
|
|
||||||
# email.parser needs the full MIME headers to parse multipart
|
|
||||||
raw = b"Content-Type: " + content_type.encode() + b"\r\n\r\n" + body
|
|
||||||
msg = email.message_from_bytes(raw, policy=email_policy.compat32)
|
|
||||||
parts = {}
|
|
||||||
for part in msg.get_payload():
|
|
||||||
cd = part.get("Content-Disposition", "")
|
|
||||||
name = None
|
|
||||||
filename = None
|
|
||||||
for item in cd.split(";"):
|
|
||||||
item = item.strip()
|
|
||||||
if item.startswith('name='):
|
|
||||||
name = item[5:].strip('"')
|
|
||||||
elif item.startswith('filename='):
|
|
||||||
filename = item[9:].strip('"')
|
|
||||||
if name is not None:
|
|
||||||
parts[name] = (filename, part.get_content_type(), part.get_payload(decode=True))
|
|
||||||
return parts
|
|
||||||
|
|
||||||
|
|
||||||
def build_multipart(fields: dict) -> tuple[bytes, str]:
|
|
||||||
"""
|
|
||||||
Build a multipart/form-data body from fields dict:
|
|
||||||
field_name -> (filename_or_None, content_type, data_bytes)
|
|
||||||
Returns (body_bytes, content_type_header_value).
|
|
||||||
"""
|
|
||||||
boundary = b"----ParakeetProxyBoundary0xDEADBEEF"
|
|
||||||
body = b""
|
|
||||||
for name, (filename, ct, data) in fields.items():
|
|
||||||
body += b"--" + boundary + b"\r\n"
|
|
||||||
if filename:
|
|
||||||
body += (
|
|
||||||
f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'
|
|
||||||
).encode()
|
|
||||||
else:
|
|
||||||
body += f'Content-Disposition: form-data; name="{name}"\r\n'.encode()
|
|
||||||
body += f"Content-Type: {ct}\r\n\r\n".encode()
|
|
||||||
body += data + b"\r\n"
|
|
||||||
body += b"--" + boundary + b"--\r\n"
|
|
||||||
return body, f"multipart/form-data; boundary={boundary.decode()}"
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyHandler(http.server.BaseHTTPRequestHandler):
|
|
||||||
def log_message(self, fmt, *args):
|
|
||||||
print(f"[parakeet-proxy] {self.address_string()} - {fmt % args}", flush=True)
|
|
||||||
|
|
||||||
def do_GET(self):
|
|
||||||
if self.path == "/health":
|
|
||||||
self._forward_get("/health")
|
|
||||||
else:
|
|
||||||
self.send_response(404)
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
def do_POST(self):
|
|
||||||
if self.path.rstrip("/") == "/v1/audio/transcriptions":
|
|
||||||
self._handle_transcription()
|
|
||||||
else:
|
|
||||||
self.send_response(404)
|
|
||||||
self.end_headers()
|
|
||||||
|
|
||||||
def _forward_get(self, path):
|
|
||||||
try:
|
|
||||||
url = f"http://127.0.0.1:{PARAKEET_PORT}{path}"
|
|
||||||
with urllib.request.urlopen(url, timeout=5) as resp:
|
|
||||||
body = resp.read()
|
|
||||||
self.send_response(resp.status)
|
|
||||||
self.send_header("Content-Type", resp.headers.get("Content-Type", "application/json"))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(body)
|
|
||||||
except Exception as e:
|
|
||||||
self.send_response(502)
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(str(e).encode())
|
|
||||||
|
|
||||||
def _handle_transcription(self):
|
|
||||||
length = int(self.headers.get("Content-Length", 0))
|
|
||||||
body = self.rfile.read(length)
|
|
||||||
ct = self.headers.get("Content-Type", "")
|
|
||||||
|
|
||||||
try:
|
|
||||||
fields = parse_multipart(ct, body)
|
|
||||||
except Exception as e:
|
|
||||||
self._error(400, f"failed to parse multipart: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if "file" not in fields:
|
|
||||||
self._error(400, "missing required field 'file'")
|
|
||||||
return
|
|
||||||
|
|
||||||
filename, file_ct, audio_data = fields["file"]
|
|
||||||
|
|
||||||
# Convert to WAV regardless of what we received
|
|
||||||
try:
|
|
||||||
wav_data = convert_to_wav(audio_data)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
self._error(400, "ffmpeg could not decode audio")
|
|
||||||
return
|
|
||||||
except Exception as e:
|
|
||||||
self._error(500, f"conversion error: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Rebuild multipart with converted WAV, preserve other fields
|
|
||||||
new_fields = {}
|
|
||||||
for name, (fn, fct, data) in fields.items():
|
|
||||||
if name == "file":
|
|
||||||
new_fields[name] = ("recording.wav", "audio/wav", wav_data)
|
|
||||||
else:
|
|
||||||
new_fields[name] = (fn, fct, data)
|
|
||||||
|
|
||||||
new_body, new_ct = build_multipart(new_fields)
|
|
||||||
|
|
||||||
# Forward to parakeet-server
|
|
||||||
try:
|
|
||||||
url = f"http://127.0.0.1:{PARAKEET_PORT}/v1/audio/transcriptions"
|
|
||||||
req = urllib.request.Request(
|
|
||||||
url,
|
|
||||||
data=new_body,
|
|
||||||
headers={"Content-Type": new_ct},
|
|
||||||
method="POST",
|
|
||||||
)
|
|
||||||
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
||||||
resp_body = resp.read()
|
|
||||||
self.send_response(resp.status)
|
|
||||||
self.send_header("Content-Type", resp.headers.get("Content-Type", "application/json"))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(resp_body)
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
resp_body = e.read()
|
|
||||||
self.send_response(e.code)
|
|
||||||
self.send_header("Content-Type", e.headers.get("Content-Type", "application/json"))
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(resp_body)
|
|
||||||
except Exception as e:
|
|
||||||
self._error(502, f"upstream error: {e}")
|
|
||||||
|
|
||||||
def _error(self, code: int, msg: str):
|
|
||||||
body = f'{{"error":{{"message":"{msg}","type":"proxy_error"}}}}'.encode()
|
|
||||||
self.send_response(code)
|
|
||||||
self.send_header("Content-Type", "application/json")
|
|
||||||
self.end_headers()
|
|
||||||
self.wfile.write(body)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
proc = subprocess.Popen([
|
|
||||||
"parakeet-server",
|
|
||||||
"--host", "127.0.0.1",
|
|
||||||
"--port", str(PARAKEET_PORT),
|
|
||||||
"--model", MODEL,
|
|
||||||
"--cache-dir", CACHE_DIR,
|
|
||||||
])
|
|
||||||
print(f"[parakeet-proxy] started parakeet-server pid={proc.pid} on :{PARAKEET_PORT}", flush=True)
|
|
||||||
|
|
||||||
server = http.server.HTTPServer(("0.0.0.0", PROXY_PORT), ProxyHandler)
|
|
||||||
print(f"[parakeet-proxy] listening on :{PROXY_PORT}", flush=True)
|
|
||||||
try:
|
|
||||||
server.serve_forever()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
proc.terminate()
|
|
||||||
proc.wait()
|
|
||||||
@@ -18,7 +18,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: download-whisper
|
- name: download-whisper
|
||||||
image: gitea.lumpiasty.xyz/lumpiasty/llama-swap:unified-vulkan-parakeet-2026-06-12
|
image: ghcr.io/mostlygeek/llama-swap:unified-vulkan-2026-05-31
|
||||||
command:
|
command:
|
||||||
- sh
|
- sh
|
||||||
- -c
|
- -c
|
||||||
@@ -48,7 +48,7 @@ spec:
|
|||||||
mountPath: /root/.cache
|
mountPath: /root/.cache
|
||||||
containers:
|
containers:
|
||||||
- name: llama-swap
|
- name: llama-swap
|
||||||
image: gitea.lumpiasty.xyz/lumpiasty/llama-swap:unified-vulkan-parakeet-2026-06-12
|
image: ghcr.io/mostlygeek/llama-swap:unified-vulkan-2026-05-31
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- llama-swap
|
- llama-swap
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ spec:
|
|||||||
# OpenAI-compatible Kokoro-FastAPI TTS server, CPU PyTorch backend.
|
# OpenAI-compatible Kokoro-FastAPI TTS server, CPU PyTorch backend.
|
||||||
# Models baked into the image (no PVC needed).
|
# Models baked into the image (no PVC needed).
|
||||||
# v0.3.0 includes fix for per-request voice tensor memory leak (#459).
|
# v0.3.0 includes fix for per-request voice tensor memory leak (#459).
|
||||||
image: ghcr.io/remsky/kokoro-fastapi-cpu:v0.5.0
|
image: ghcr.io/remsky/kokoro-fastapi-cpu:v0.4.0
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8880
|
- containerPort: 8880
|
||||||
name: http
|
name: http
|
||||||
|
|||||||
@@ -13,4 +13,3 @@ configMapGenerator:
|
|||||||
namespace: llama
|
namespace: llama
|
||||||
files:
|
files:
|
||||||
- config.yaml=configs/config.yaml
|
- config.yaml=configs/config.yaml
|
||||||
- parakeet-proxy.py=configs/parakeet-proxy.py
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
initContainers:
|
initContainers:
|
||||||
- name: prepare-home
|
- name: prepare-home
|
||||||
image: alpine:3.24.0
|
image: alpine:3.23.4
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ spec:
|
|||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: open-webui
|
chart: open-webui
|
||||||
version: 14.8.0
|
version: 14.6.0
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: HelmRepository
|
kind: HelmRepository
|
||||||
name: open-webui
|
name: open-webui
|
||||||
@@ -74,17 +74,7 @@ spec:
|
|||||||
value: "false"
|
value: "false"
|
||||||
- name: OAUTH_MERGE_ACCOUNTS_BY_EMAIL
|
- name: OAUTH_MERGE_ACCOUNTS_BY_EMAIL
|
||||||
value: "true"
|
value: "true"
|
||||||
- name: ENABLE_OAUTH_ROLE_MANAGEMENT
|
# STT via whisper-server (routed through llama-swap)
|
||||||
value: "true"
|
|
||||||
- name: OAUTH_ROLES_CLAIM
|
|
||||||
value: "groups"
|
|
||||||
- name: OAUTH_ADMIN_ROLES
|
|
||||||
value: "Admins"
|
|
||||||
- name: OAUTH_ALLOWED_ROLES
|
|
||||||
value: "Users"
|
|
||||||
- name: OAUTH_AUTO_REDIRECT
|
|
||||||
value: "true"
|
|
||||||
# STT via parakeet-server (routed through llama-swap)
|
|
||||||
- name: AUDIO_STT_ENGINE
|
- name: AUDIO_STT_ENGINE
|
||||||
value: "openai"
|
value: "openai"
|
||||||
- name: AUDIO_STT_OPENAI_API_BASE_URL
|
- name: AUDIO_STT_OPENAI_API_BASE_URL
|
||||||
@@ -92,7 +82,9 @@ spec:
|
|||||||
- name: AUDIO_STT_OPENAI_API_KEY
|
- name: AUDIO_STT_OPENAI_API_KEY
|
||||||
value: "ignored"
|
value: "ignored"
|
||||||
- name: AUDIO_STT_MODEL
|
- name: AUDIO_STT_MODEL
|
||||||
value: "parakeet-tdt_ctc-1.1b"
|
value: "whisper-small"
|
||||||
|
- name: AUDIO_STT_SUPPORTED_CONTENT_TYPES
|
||||||
|
value: "audio/wav,audio/wave"
|
||||||
# TTS via OuteTTS (routed through llama-swap)
|
# TTS via OuteTTS (routed through llama-swap)
|
||||||
# TTS via dedicated Kokoro server (CPU-only, separate pod)
|
# TTS via dedicated Kokoro server (CPU-only, separate pod)
|
||||||
- name: AUDIO_TTS_ENGINE
|
- name: AUDIO_TTS_ENGINE
|
||||||
|
|||||||
@@ -1,492 +0,0 @@
|
|||||||
# App deployment guidelines
|
|
||||||
|
|
||||||
This document summarizes current guidelines, requirements, common patterns, and standards that newly deployed apps should meet.
|
|
||||||
|
|
||||||
## Structure
|
|
||||||
|
|
||||||
Each app on cluster should be contained in its own kustomization living in subdirectory under [apps](/apps) and imported from main [apps kustomization](/apps/kustomization.yaml). Apps that provide infrastructural services belong to [infra](/infra). Few examples:
|
|
||||||
|
|
||||||
- **Open WebUI**: Web app, belongs in [apps/openwebui](/apps/openwebui/) together with its direct and unique dependencies eg. database
|
|
||||||
- **llama-swap** (llama.cpp + whisper + stablediffusion): Inference server, service used by other deployments on cluster but does not manages cluster, belongs in [apps/llama](/apps/llama/)
|
|
||||||
- **kokoro**: Text to speech inference server, also service used by other deployments, I consider it closely related to llama-swap, so due to arbitrary decision, keeping it together with llama-swap under [apps/llama](/apps/llama/)
|
|
||||||
- **crawl4ai**: Web scraper, another service used only by other apps, belongs in [apps/crawl4ai](/apps/crawl4ai/)
|
|
||||||
- **Gitea**: Code forge, despite being essential for overall architecture (holding cluster's code) is not a core cluster software, belongs in [apps/gitea](/apps/gitea/)
|
|
||||||
- **Woodpecker**: Continous Integration system, belongs in [apps/woodpecker](/apps/woodpecker/)
|
|
||||||
- **Cilium**: Kubernetes CNI, core cluster functionality, belongs in [infra/controllers/cilium.yaml](/infra/controllers/cilium.yaml)
|
|
||||||
- **Nginx Ingress Controller**: Provides ingress kubernetes functionality, belongs in [infra/controllers/nginx-ingress.yaml](/infra/controllers/nginx-ingress.yaml)
|
|
||||||
- **CloudNativePG**: Kubernetes PostgreSQL operator, belongs in [infra/controllers/cloudnative-pg.yaml](/infra/controllers/cloudnative-pg.yaml)
|
|
||||||
- **OpenBao** Secret storage and Kubernetes operator, belongs in [infra/controllers/openbao.yaml](/infra/controllers/openbao.yaml)
|
|
||||||
|
|
||||||
Kustomizations are reconciled on `git push` by flux running on cluster, triggered by [Woodpecker job](/.woodpecker/flux-reconcile-source.yaml). App Kustomization should import all resources related to app in `kustomization.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- namespace.yaml
|
|
||||||
- pvc.yaml
|
|
||||||
- release.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Namespace
|
|
||||||
|
|
||||||
Each app kustomization should have its own kubernetes namespace to contain all resources related to app in `namespace.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: immich
|
|
||||||
```
|
|
||||||
|
|
||||||
## Helm charts
|
|
||||||
|
|
||||||
If app is distributed via Helm chart, you can deploy it using flux HelmRepository and HelmRelease resources like in following example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
apiVersion: source.toolkit.fluxcd.io/v1
|
|
||||||
kind: HelmRepository
|
|
||||||
metadata:
|
|
||||||
name: secustor
|
|
||||||
namespace: immich
|
|
||||||
spec:
|
|
||||||
interval: 24h
|
|
||||||
url: https://secustor.dev/helm-charts
|
|
||||||
---
|
|
||||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
|
||||||
kind: HelmRelease
|
|
||||||
metadata:
|
|
||||||
name: immich
|
|
||||||
namespace: immich
|
|
||||||
spec:
|
|
||||||
interval: 30m
|
|
||||||
chart:
|
|
||||||
spec:
|
|
||||||
chart: immich
|
|
||||||
version: 1.2.6
|
|
||||||
sourceRef:
|
|
||||||
kind: HelmRepository
|
|
||||||
name: secustor
|
|
||||||
values:
|
|
||||||
<values>
|
|
||||||
```
|
|
||||||
|
|
||||||
If the app does not have a helm repository, but helm chart is available in git repository directly in repository, you can make use of it using GitRepository flux source:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
apiVersion: source.toolkit.fluxcd.io/v1
|
|
||||||
kind: GitRepository
|
|
||||||
metadata:
|
|
||||||
name: kaneo
|
|
||||||
namespace: kaneo
|
|
||||||
spec:
|
|
||||||
interval: 24h
|
|
||||||
url: https://github.com/usekaneo/kaneo.git
|
|
||||||
ref:
|
|
||||||
tag: v2.7.5
|
|
||||||
ignore: |
|
|
||||||
# exclude all
|
|
||||||
/*
|
|
||||||
# include charts directory
|
|
||||||
!/charts/
|
|
||||||
```
|
|
||||||
|
|
||||||
You can use third-party helm charts to deploy applications, consider this possibility if:
|
|
||||||
|
|
||||||
- There is no official helm chart for the application
|
|
||||||
- The official helm chart is unmaintained
|
|
||||||
- The official helm chart is using glaring bad practices
|
|
||||||
- The official helm chart is missing configuration options for what we need
|
|
||||||
|
|
||||||
When deciding which helm chart to use, watch out for following things in particular:
|
|
||||||
|
|
||||||
- Development activity, stability, maturity
|
|
||||||
- Whether the app deployed by chart is up to date - automated updates are large bonus
|
|
||||||
- Unresolved / breaking issues
|
|
||||||
- Configurability, can we configure things we need, disable undesired features
|
|
||||||
|
|
||||||
When configuring Helm chart, keep in mind:
|
|
||||||
- Do not use bundled PVCs, bring our own one or at least configure chart to bind it to manually created `PersistentVolume` according to [Data / PVCs pattern](#data--pvcs-pattern)
|
|
||||||
- Do not use bundled Postgres database unless the chart is using CloudNativePG's Cluster resource, bring our own one using [Postgres operator](#postgres-operator)
|
|
||||||
- do not
|
|
||||||
|
|
||||||
## Bare Kubernetes deployments
|
|
||||||
|
|
||||||
If:
|
|
||||||
|
|
||||||
- the app is not packaged as a helm chart or
|
|
||||||
- it would be simpler to deploy it without package (for example custom privileged pod with access to gpu) or
|
|
||||||
- the app is so simple it doesn't make sense to make helm package it (for example, simple http proxy that alters headers or stateless single-binary app) or
|
|
||||||
- for any other reason it would make more sense to skip helm
|
|
||||||
|
|
||||||
You can deploy app skipping helm chart and just create raw Kubernetes manifests like Deployment, StatefulSet and other supporting resources like ConfigMap, Service, Ingress directly.
|
|
||||||
|
|
||||||
## Data / PVCs pattern
|
|
||||||
|
|
||||||
Data are stored on local disk of node using OpenEBS LVM LocalPV. To create a persistent volume, use following example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
apiVersion: local.openebs.io/v1alpha1
|
|
||||||
kind: LVMVolume
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
kubernetes.io/nodename: anapistula-delrosalae
|
|
||||||
name: immich-library-lvmhdd
|
|
||||||
namespace: openebs
|
|
||||||
spec:
|
|
||||||
capacity: 150Gi
|
|
||||||
ownerNodeID: anapistula-delrosalae
|
|
||||||
shared: "yes"
|
|
||||||
thinProvision: "no"
|
|
||||||
vgPattern: ^openebs-hdd$
|
|
||||||
volGroup: openebs-hdd
|
|
||||||
---
|
|
||||||
kind: PersistentVolume
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: immich-library-lvmhdd
|
|
||||||
spec:
|
|
||||||
capacity:
|
|
||||||
storage: 150Gi
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
persistentVolumeReclaimPolicy: Retain
|
|
||||||
storageClassName: hdd-lvmpv
|
|
||||||
volumeMode: Filesystem
|
|
||||||
csi:
|
|
||||||
driver: local.csi.openebs.io
|
|
||||||
fsType: btrfs
|
|
||||||
volumeHandle: immich-library-lvmhdd
|
|
||||||
---
|
|
||||||
kind: PersistentVolumeClaim
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
name: library-lvmhdd
|
|
||||||
namespace: immich
|
|
||||||
spec:
|
|
||||||
accessModes:
|
|
||||||
- ReadWriteOnce
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 150Gi
|
|
||||||
storageClassName: hdd-lvmpv
|
|
||||||
volumeName: immich-library-lvmhdd
|
|
||||||
```
|
|
||||||
|
|
||||||
Create LVMVolume and PersistentVolume resources manually and **do not** rely on automatic scheduling of PVCs because we want created LVM LVs on disk to have deterministic names and be reused if already exist on disk, which scheduler does not give us. There are two LVM storage classes:
|
|
||||||
|
|
||||||
- **hdd-lvmpv**, volume group: openebs-hdd, use for bulk data, like media library
|
|
||||||
- **ssd-lvmpv**, volume group: openebs-ssd, use for small datasets that benefit from quick storage access like databases, state data etc.
|
|
||||||
|
|
||||||
When deciding the size of the volume, make minimal prediction, starting with 1GiB if you do not predict app to use much disk space.
|
|
||||||
|
|
||||||
## Vault secrets
|
|
||||||
|
|
||||||
There is OpenBao installed on cluster that manages access to secrets. The KV2 secret engine is mounted at `secret`, use it to store static secrets like API keys to external services, passwords and other entries you do not want to keep in plaintext in git repository.
|
|
||||||
|
|
||||||
To access the KV secrets on cluster, use Vault Secrets Operator installed on cluster, which provides `VaultStaticSecret` custom resource that syncs a path from OpenBao to Kubernetes `Secret` object.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: llama-proxy
|
|
||||||
namespace: llama
|
|
||||||
---
|
|
||||||
apiVersion: secrets.hashicorp.com/v1beta1
|
|
||||||
kind: VaultAuth
|
|
||||||
metadata:
|
|
||||||
name: llama
|
|
||||||
namespace: llama
|
|
||||||
spec:
|
|
||||||
method: kubernetes
|
|
||||||
mount: kubernetes
|
|
||||||
kubernetes:
|
|
||||||
role: llama-proxy
|
|
||||||
serviceAccount: llama-proxy
|
|
||||||
---
|
|
||||||
apiVersion: secrets.hashicorp.com/v1beta1
|
|
||||||
kind: VaultStaticSecret
|
|
||||||
metadata:
|
|
||||||
name: llama-api-key
|
|
||||||
namespace: llama
|
|
||||||
spec:
|
|
||||||
type: kv-v2
|
|
||||||
|
|
||||||
mount: secret
|
|
||||||
path: ollama
|
|
||||||
|
|
||||||
destination:
|
|
||||||
create: true
|
|
||||||
name: llama-api-key
|
|
||||||
type: Opaque
|
|
||||||
transformation:
|
|
||||||
excludeRaw: true
|
|
||||||
|
|
||||||
vaultAuthRef: llama
|
|
||||||
```
|
|
||||||
|
|
||||||
To give access to specified secret for given k8s ServiceAccount, you need to create kubernetes auth role and policy. Create a kubernetes auth role named `llama-proxy`, by creating file `vault/kubernetes-auth-roles/llama-proxy.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
bound_service_account_names:
|
|
||||||
- llama-proxy
|
|
||||||
bound_service_account_namespaces:
|
|
||||||
- llama
|
|
||||||
token_policies:
|
|
||||||
- ollama
|
|
||||||
```
|
|
||||||
|
|
||||||
Create policy named `ollama` by creating file `vault/policy/ollama.hcl`:
|
|
||||||
|
|
||||||
```hcl
|
|
||||||
path "secret/data/ollama" {
|
|
||||||
capabilities = ["read"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Once these files are created, ask operator to reconcile OpenBao configuration and create required secret.
|
|
||||||
|
|
||||||
## Postgres operator
|
|
||||||
|
|
||||||
There is CloudNativePG operator installed on cluster that manages databases of applications running on cluster. You can create Postgres database by creating `Cluster` resource:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
apiVersion: postgresql.cnpg.io/v1
|
|
||||||
kind: Cluster
|
|
||||||
metadata:
|
|
||||||
name: kaneo-db
|
|
||||||
namespace: kaneo
|
|
||||||
spec:
|
|
||||||
instances: 1
|
|
||||||
|
|
||||||
storage:
|
|
||||||
pvcTemplate:
|
|
||||||
storageClassName: ssd-lvmpv
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
storage: 10Gi
|
|
||||||
volumeName: kaneo-db-1
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a `PersistentVolume` and `LVMVol` resources manually but **do not** create `PersistentVolumeClaim`, CloudNativePG will create one on its own referencing `PersistentVolume` specified in `volumeName`. Do not replicate the database, there is only one node in the cluster currently. The `Cluster` resource will automatically create secret, use it to configure app:
|
|
||||||
|
|
||||||
```
|
|
||||||
Name: kaneo-db-app
|
|
||||||
Namespace: kaneo
|
|
||||||
Labels: app.kubernetes.io/managed-by=cloudnative-pg
|
|
||||||
cnpg.io/cluster=kaneo-db
|
|
||||||
cnpg.io/reload=true
|
|
||||||
cnpg.io/userType=app
|
|
||||||
Annotations: cnpg.io/operatorVersion: 1.29.1
|
|
||||||
|
|
||||||
Type: kubernetes.io/basic-auth
|
|
||||||
|
|
||||||
Data
|
|
||||||
====
|
|
||||||
dbname: 3 bytes
|
|
||||||
fqdn-jdbc-uri: 145 bytes
|
|
||||||
fqdn-uri: 126 bytes
|
|
||||||
host: 11 bytes
|
|
||||||
jdbc-uri: 127 bytes
|
|
||||||
password: 64 bytes
|
|
||||||
pgpass: 90 bytes
|
|
||||||
port: 4 bytes
|
|
||||||
uri: 108 bytes
|
|
||||||
user: 3 bytes
|
|
||||||
username: 3 bytes
|
|
||||||
```
|
|
||||||
|
|
||||||
## LoadBalancers
|
|
||||||
|
|
||||||
You can expose installed app to the Internet using Cilium's LoadBalancer configured on cluster:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: teamspeak3
|
|
||||||
namespace: ispeak3
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: teamspeak3
|
|
||||||
ports:
|
|
||||||
- name: voice
|
|
||||||
protocol: UDP
|
|
||||||
port: 9987
|
|
||||||
targetPort: 9987
|
|
||||||
- name: filetransfer
|
|
||||||
protocol: TCP
|
|
||||||
port: 30033
|
|
||||||
targetPort: 30033
|
|
||||||
type: LoadBalancer
|
|
||||||
externalTrafficPolicy: Local
|
|
||||||
ipFamilyPolicy: PreferDualStack
|
|
||||||
```
|
|
||||||
|
|
||||||
IPv6 will be directly reachable from the internet by its assigned address, for IPv4 currently you need to configure port forward on router in `ansible/roles/routeros/firewall.yml`, that step is not yet automated. The assigned internal IP will be known after manifests are applied on cluster. For this reason, there is no ExternalDNS configured yet, if you need a DNS name, ask the operator to configure DNS name for LoadBalancer. Assign names from lumpiasty.xyz subdomains (eg. kaneo.lumpiasty.xyz) unless explicitly requested. Do not use LoadBalancer for exposing HTTP applications, use Ingress instead.
|
|
||||||
|
|
||||||
## Ingress
|
|
||||||
|
|
||||||
You can expose HTTP applications using NGINX Ingress Controller:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
namespace: llama
|
|
||||||
name: llama
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt
|
|
||||||
acme.cert-manager.io/http01-edit-in-place: "true"
|
|
||||||
nginx.ingress.kubernetes.io/proxy-buffering: "false"
|
|
||||||
nginx.ingress.kubernetes.io/proxy-read-timeout: 30m
|
|
||||||
nginx.ingress.kubernetes.io/proxy-body-size: 8m
|
|
||||||
spec:
|
|
||||||
ingressClassName: nginx-ingress
|
|
||||||
rules:
|
|
||||||
- host: llama.lumpiasty.xyz
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- backend:
|
|
||||||
service:
|
|
||||||
name: llama-proxy
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
path: /
|
|
||||||
pathType: Prefix
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- llama.lumpiasty.xyz
|
|
||||||
secretName: llama-ingress
|
|
||||||
```
|
|
||||||
|
|
||||||
TLS certificates are automatically issued for subdomains of lumpiasty.xyz using cert-manager. DNS name assignment is not automatic yet, ask operator to create DNS name for ingress resources.
|
|
||||||
|
|
||||||
## Keeping app up to date
|
|
||||||
|
|
||||||
There is a Renovate job configured for this repository as [Woodpecker job](/.woodpecker/renovate.yaml) to keep applications up to date. Renovate automatically keeps track of:
|
|
||||||
|
|
||||||
- Docker images specified in Kubernetes manifests like Deployment, StatefulSet etc
|
|
||||||
- HelmRelease versions
|
|
||||||
- GitRepository tags
|
|
||||||
|
|
||||||
To make Renovate automatically update applications, always specify full versions of docker images or helm chart release. If you use ambigous tags, renovate will not have chance to update and the cluster will never download new image because this tag already existed on node. **Do not** use:
|
|
||||||
|
|
||||||
- latest (or its variants like stable, current, main, master current)
|
|
||||||
- "Sliding" versions, like 1 or 1.2 that point at 1.2.1 currently and will change image it points at when version 1.2.2 is released
|
|
||||||
|
|
||||||
As a last resort if the application does not publish stable image tags, pin digest of image.
|
|
||||||
|
|
||||||
Renovate may require custom configuration if:
|
|
||||||
|
|
||||||
- App is using non-standard versioning schema
|
|
||||||
|
|
||||||
Example app versioned by date (unified-vulkan-2026-01-01), renovate.json:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"matchDatasources": ["docker"],
|
|
||||||
"matchPackageNames": ["ghcr.io/mostlygeek/llama-swap"],
|
|
||||||
"versioning": "regex:^unified-vulkan-(?<major>\\d{4})-(?<minor>\\d{2})-(?<patch>\\d{2})$",
|
|
||||||
"automerge": true,
|
|
||||||
"automergeType": "pr",
|
|
||||||
"platformAutomerge": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- Docker image tag is specified in non-standard field that Renovate may not recognise automatically such as Helm values
|
|
||||||
|
|
||||||
Example app with non-standard image selected in helm values instead of image's default (which is latest in this chart):
|
|
||||||
```yaml
|
|
||||||
values:
|
|
||||||
kaneo:
|
|
||||||
image:
|
|
||||||
tag: "2.7.3" # renovate: depName=ghcr.io/usekaneo/kaneo registryUrl=https://ghcr.io
|
|
||||||
```
|
|
||||||
|
|
||||||
Renovate is configured so it automatically merges patch versions, other updates are created as pull requests to be manually reviewed and merged unless explicitly desired on per case basis.
|
|
||||||
|
|
||||||
## SSO / OIDC / Authentik
|
|
||||||
|
|
||||||
There is an Authentik running on cluster providing SSO for applications. Configure user-facing apps to utilize it correctly.
|
|
||||||
|
|
||||||
Authentik supports following protocols:
|
|
||||||
|
|
||||||
- OAuth2 / OpenID Connect
|
|
||||||
- SAML
|
|
||||||
- Radius
|
|
||||||
- LDAP
|
|
||||||
- SCIM
|
|
||||||
|
|
||||||
Currently, there is no Authentik configuration in code, ask operator to create application in the UI and save OAuth id and secret in OpenBao under `secret/authentik/<app>`. Authentik provides discovery URL for OAuth applications: `https://authentik.lumpiasty.xyz/application/o/<app slug>/.well-known/openid-configuration`.
|
|
||||||
|
|
||||||
Configure the app to disable guest access, built-in registration and automatically register unprivileged users with `user` role and privileged users with `admin` role as the app allows.
|
|
||||||
|
|
||||||
## Privileged apps
|
|
||||||
|
|
||||||
Some apps require direct access to devices, like GPU. There are no specific operators yet, apps that require access to GPU are simply launched as privileged pods, example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: llama-swap
|
|
||||||
namespace: llama
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: Recreate
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: llama-swap
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: llama-swap
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: llama-swap
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /dev/kfd
|
|
||||||
name: kfd
|
|
||||||
- mountPath: /dev/dri
|
|
||||||
name: dri
|
|
||||||
securityContext:
|
|
||||||
privileged: true
|
|
||||||
volumes:
|
|
||||||
- name: kfd
|
|
||||||
hostPath:
|
|
||||||
path: /dev/kfd
|
|
||||||
type: CharDevice
|
|
||||||
- name: dri
|
|
||||||
hostPath:
|
|
||||||
path: /dev/dri
|
|
||||||
type: Directory
|
|
||||||
```
|
|
||||||
|
|
||||||
Creating of such pods is forbidden unless explicitly allowed in Talos config:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# CSI driver requirement
|
|
||||||
cluster:
|
|
||||||
apiServer:
|
|
||||||
admissionControl:
|
|
||||||
- name: PodSecurity
|
|
||||||
configuration:
|
|
||||||
apiVersion: pod-security.admission.config.k8s.io/v1beta1
|
|
||||||
kind: PodSecurityConfiguration
|
|
||||||
exemptions:
|
|
||||||
namespaces:
|
|
||||||
- llama
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the patch like this under `talos/patches/<app>.patch`, add it to `gen-talos-config` target in Makefile and ask operator to apply reconcile Talos config to allow privileged pods in specified namespace.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_137_2)">
|
|
||||||
<rect width="136" height="136" fill="#141414"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M62.6855 103.724C58.5716 104.595 56.0001 103.265 56 101.66L56 70.0264C56 69.8606 56.0032 69.686 56.0088 69.5069C56.0039 69.3848 56.001 69.249 56.001 69.0977L56.001 37.9444C56.015 36.6524 56.2588 35.7449 59.2588 35.1094L73.3145 32.2764C77.4285 31.405 80 32.7365 80 34.3408L80 65.9746C80 66.1409 79.9978 66.3155 79.9922 66.4951C79.997 66.6169 79.999 66.7526 79.999 66.9033L79.999 98.0567C79.9849 99.3483 79.7408 100.256 76.7412 100.892L62.6855 103.724Z" fill="#F5F5F5"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.6855 111.723C26.5716 112.594 24.0001 111.264 24 109.659L24 78.0244C24 77.8588 24.0032 77.6848 24.0088 77.5059C24.0039 77.3838 24.001 77.248 24.001 77.0967L24.001 45.9434C24.015 44.6514 24.2588 43.7439 27.2588 43.1084L41.3145 40.2754C45.4285 39.404 48 40.7355 48 42.3399L48 73.9737C48 74.1399 47.9978 74.3146 47.9922 74.4942C47.997 74.6159 47.999 74.7517 47.999 74.9024L47.999 106.056C47.9849 107.347 47.7408 108.255 44.7412 108.891L30.6855 111.723Z" fill="#F5F5F5"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M105.314 24.2754C109.428 23.404 112 24.7355 112 26.3398V37.1845L94.0576 60.5019L111.999 82.7802V90.0576C111.985 91.3492 111.741 92.2571 108.741 92.8925L94.6855 95.7246C90.5717 96.596 88.0002 95.2654 88 93.6611V62.0254C88 61.8598 88.0032 61.6856 88.0088 61.5068C88.0039 61.3848 88.001 61.2488 88.001 61.0976V29.9433C88.0151 28.6516 88.2591 27.7438 91.2588 27.1084L105.314 24.2754Z" fill="#F5F5F5"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_137_2">
|
|
||||||
<rect width="136" height="136" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,124 +0,0 @@
|
|||||||
# CoreDNS DNS64 + NAT64 — design and implementation
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
|
|
||||||
Replace the RouterOS built-in DNS forwarder with CoreDNS and implement IPv6-mostly networking (RFC 8925) using DNS64 + NAT64, allowing clients to phase out IPv4 while maintaining full connectivity to IPv4-only destinations.
|
|
||||||
|
|
||||||
## Background
|
|
||||||
|
|
||||||
The network uses Hurricane Electric as an IPv6 tunnel broker (`2001:470:61a3::/48`). HE assigns addresses from datacenter IP ranges, causing some websites to serve endless CAPTCHAs or flag connections as bot traffic. IPv6-mostly solves this differently: capable clients prefer IPv6 natively, and IPv4-only destinations are reached through NAT64 — using our own IPv4 WAN address rather than HE's, avoiding the datacenter flagging problem for those destinations.
|
|
||||||
|
|
||||||
## How it works
|
|
||||||
|
|
||||||
```
|
|
||||||
IPv6-only client CoreDNS (DNS64) NAT64 (Tayga)
|
|
||||||
│ │ │
|
|
||||||
│── AAAA? example.com ──────────▶│ │
|
|
||||||
│ │── A? example.com ─────────▶ upstream
|
|
||||||
│ │◀── 93.184.216.34 ──────────│
|
|
||||||
│◀── 64:ff9b::5db8:d822 ─────────│ (synthesized AAAA) │
|
|
||||||
│ │ │
|
|
||||||
│── TCP SYN to 64:ff9b::5db8:d822 ──────────────────────────▶│
|
|
||||||
│ │ (RouterOS routes │
|
|
||||||
│ │ 64:ff9b::/96 │
|
|
||||||
│ │ to Tayga) │
|
|
||||||
│ │ │── TCP SYN to 93.184.216.34
|
|
||||||
│ │ │◀─ TCP SYN-ACK
|
|
||||||
│◀── TCP SYN-ACK (translated) ───────────────────────────────│
|
|
||||||
```
|
|
||||||
|
|
||||||
For all destinations — including sites with real AAAA records — DNS64 overrides the response with a synthesized `64:ff9b::/96` address. All traffic routes through Tayga and exits on our own IPv4 WAN address, bypassing the HE tunnel broker. This eliminates the datacenter IP flagging and CAPTCHA loops that HE addresses trigger on some sites.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
### CoreDNS (custom build)
|
|
||||||
|
|
||||||
Built from source with 7 plugins instead of the default ~40, reducing the compressed image from ~20 MB to ~6-8 MB. This matters for fitting on the CRS internal flash.
|
|
||||||
|
|
||||||
Plugin set: `errors`, `log`, `health`, `cache`, `dns64`, `forward`, `reload`.
|
|
||||||
|
|
||||||
Plugin order in `plugin.cfg` determines execution order. `dns64` must come before `forward` so it can intercept AAAA responses from upstream rather than letting `forward` return them directly to the client.
|
|
||||||
|
|
||||||
Source: [`mikrotik/coredns/`](../mikrotik/coredns/)
|
|
||||||
|
|
||||||
The `dns64` plugin is built into CoreDNS — no external plugin needed. It performs the A→AAAA synthesis using the well-known prefix `64:ff9b::/96` (RFC 6052).
|
|
||||||
|
|
||||||
`translate_all` and `allow_ipv4` are both set. Without `allow_ipv4`, the plugin only intercepts queries arriving over IPv6 — dual-stack clients querying CoreDNS over IPv4 (the common case, since the router forwards DNS via IPv4) would receive real AAAA records and use the HE tunnel instead of NAT64.
|
|
||||||
|
|
||||||
| Client type | AAAA query handling | A query handling |
|
|
||||||
|---|---|---|
|
|
||||||
| IPv6-only (CLAT) | synthesized `64:ff9b::` → NAT64 path | not asked; client has no IPv4 stack |
|
|
||||||
| Dual-stack (no CLAT) | synthesized `64:ff9b::` → NAT64 path | `forward` returns real A → client uses IPv4 directly |
|
|
||||||
| IPv4-only (no IPv6) | synthesized `64:ff9b::` → client ignores it (no IPv6 stack), uses A record | `forward` returns real A → client uses IPv4 directly |
|
|
||||||
|
|
||||||
IPv4-only clients receive synthesized AAAA records but their stack cannot use them — they fall back to A records normally. No breakage.
|
|
||||||
|
|
||||||
### Tayga (NAT64)
|
|
||||||
|
|
||||||
Stateless IP/ICMP translation (SIIT, RFC 7915). Receives IPv6 packets for `64:ff9b::/96`, strips the prefix to get the IPv4 destination, rewrites the packet headers, and routes it out as IPv4. Return traffic gets the inverse translation.
|
|
||||||
|
|
||||||
RouterOS does not implement NAT64 natively (confirmed in official docs). The approach described in some blog posts of writing per-destination `/ipv6 firewall nat dst-nat` rules is not real NAT64 — it is static port forwarding and requires manually enumerating every destination.
|
|
||||||
|
|
||||||
Official image: `ghcr.io/apalrd/tayga` — no custom build needed.
|
|
||||||
|
|
||||||
### RouterOS
|
|
||||||
|
|
||||||
Provides:
|
|
||||||
- Static IPv6 route `64:ff9b::/96 → Tayga`
|
|
||||||
- Masquerade of Tayga's IPv4 pool to WAN
|
|
||||||
- PREF64 option in Router Advertisements (`/ipv6/nd pref64`)
|
|
||||||
- PREF64 + RDNSS options in Router Advertisements (per-interface `ipv6 nd` entries)
|
|
||||||
- DHCP option 108 to signal IPv6-only preference to capable clients (sent only when requested)
|
|
||||||
|
|
||||||
## Client behaviour with DHCPv4 option 108
|
|
||||||
|
|
||||||
Option 108 and PREF64 work as a pair — deploying one without the other breaks clients:
|
|
||||||
|
|
||||||
- **Option 108** (RFC 8925): tells capable clients to drop IPv4. RouterOS only sends it to clients that request code 108 in their Parameter Request List (that is what the `force` flag on the option controls — we leave it unset). Legacy clients never see it.
|
|
||||||
- **PREF64 in RA** (RFC 8781): tells the now IPv6-only client the NAT64 prefix so it can activate CLAT. Without PREF64, a client that honoured option 108 has no working translation and appears stuck "obtaining IP address".
|
|
||||||
- **RDNSS in RA** (RFC 8106): IPv6-only clients ignore DHCPv4 entirely, including its `dns-server`. They need an IPv6 DNS address from RA. We advertise the router's per-VLAN IPv6 address; RouterOS DNS forwards to CoreDNS.
|
|
||||||
|
|
||||||
| Client OS | Behaviour |
|
|
||||||
|---|---|
|
|
||||||
| iOS 16+, macOS 13+ | Requests 108, drops IPv4, activates CLAT via PREF64 |
|
|
||||||
| Android 10+ | Requests 108, drops IPv4, activates CLAT via PREF64 |
|
|
||||||
| Windows 11 (preview) | Partial — CLAT support in preview as of 2026 |
|
|
||||||
| Linux (NetworkManager) | Honours option 108; CLAT requires PREF64 |
|
|
||||||
| Legacy/unaware devices | Never request 108, receive IPv4 lease normally, dual-stack |
|
|
||||||
|
|
||||||
Option 108 value is a 32-bit seconds timer (V6ONLY_WAIT, minimum 300 per RFC), refreshed on each DHCP renewal. We use 86400 (1 day) so a failed DNS64/NAT64 stack self-heals within a day by clients falling back to IPv4.
|
|
||||||
|
|
||||||
### Deployment pitfalls (learned the hard way)
|
|
||||||
|
|
||||||
Option 108 must never be deployed before the whole IPv6-only path works end to end. A client that honours it drops IPv4 immediately and depends on RA-provided PREF64 + RDNSS and a working NAT64. Each of these failure modes was hit in sequence, and every one presented identically on the phone ("stuck obtaining IP address" / "failed to connect"):
|
|
||||||
|
|
||||||
1. **ND entries silently not created.** RouterOS ships only the `interface=all` default in `/ipv6/nd`. An `api_find_and_modify` task searching for `interface=vlan2` matches zero entries and silently succeeds (`require_matches_min` defaults to 0) — PREF64 was never advertised. Use `api_modify`, which creates missing entries.
|
|
||||||
2. **RDNSS pointing at a nonexistent address.** VLAN IPv6 addresses came `from-pool`, so the actual prefix was dynamic (`:0::/64`), while the ND `dns=` advertised the documented-but-wrong `:9::/64` router address. Fixed by switching VLANs to static addressing — the HE prefix is static, the pool indirection served no purpose.
|
|
||||||
3. **`advertise-dns=no` on new ND entries.** RouterOS creates per-interface ND entries with `advertise-dns=no`, which suppresses the RDNSS option entirely — even when a static `dns=` list is configured on the entry. Must be set to `yes` explicitly.
|
|
||||||
|
|
||||||
4. **RouterOS static FWD entries corrupt NXDOMAIN.** A manually added `type=FWD match-subdomain=yes` entry for `lumpiasty.xyz` (intended to bypass DNS64 for our own zone) returned `NOERROR` with an empty answer for nonexistent subdomains instead of relaying NXDOMAIN. Combined with `ndots:5` and the `homelab-infra.lumpiasty.xyz` search domain in kubernetes pods, `getaddrinfo` received NODATA for the search-suffixed candidate (`authentik.lumpiasty.xyz.homelab-infra.lumpiasty.xyz`), concluded the name exists, stopped the search loop, and never tried the absolute name — apps failed with `ENOTFOUND` for perfectly valid hostnames while `nslookup` (absolute query) worked. The zone bypass now lives in the CoreDNS Corefile as a dedicated `lumpiasty.xyz:53` server block without `dns64`, which relays rcodes faithfully. RouterOS DNS does plain forwarding only; no FWD entries except Tailscale MagicDNS.
|
|
||||||
|
|
||||||
Verification tooling: `rdisc6` (NixOS package `ndisc6`) shows the exact RA contents — RDNSS and PREF64 must both be present. When capturing DHCP in Wireshark, do not filter by client MAC: OFFER/ACK are sent to the broadcast MAC and disappear from the capture, hiding the server side of the exchange. When diagnosing DNS, the CoreDNS `log` plugin output is visible via `/log print` on the router (container `logging=yes`) and includes the rcode CoreDNS returned — comparing it with what the client received isolates which hop corrupts responses. Beware misleading test names: `*.example.com` legitimately returns NODATA upstream, making it useless for NXDOMAIN testing.
|
|
||||||
|
|
||||||
## CI/CD
|
|
||||||
|
|
||||||
The Woodpecker pipeline at [`.woodpecker/coredns-build.yaml`](../.woodpecker/coredns-build.yaml) triggers on any push that touches `mikrotik/coredns/**`. It:
|
|
||||||
|
|
||||||
1. Authenticates to OpenBao using the shared Renovate AppRole (`renovate_role_id` / `renovate_secret_id` Woodpecker secrets)
|
|
||||||
2. Fetches registry credentials from the `container-registry` KV secret (`REGISTRY_USERNAME` / `REGISTRY_PASSWORD`)
|
|
||||||
3. Builds the `linux/arm64` image using `docker buildx`
|
|
||||||
4. Pushes `latest` and a short-SHA tag to `gitea.lumpiasty.xyz/<owner>/coredns-mikrotik`
|
|
||||||
5. Revokes the OpenBao token
|
|
||||||
|
|
||||||
To update the CoreDNS version: change the `--branch` argument in the Dockerfile `git clone` line.
|
|
||||||
|
|
||||||
## RouterOS deployment
|
|
||||||
|
|
||||||
See [`mikrotik/README.md`](../mikrotik/README.md) for the full set of RouterOS commands.
|
|
||||||
|
|
||||||
## Known limitations
|
|
||||||
|
|
||||||
- **DNSSEC**: The `dns64` plugin does not validate DNSSEC on synthesized responses (upstream bug noted in the plugin docs). If DNSSEC is required, run a validating resolver upstream and disable synthesis for signed zones.
|
|
||||||
- **IPv4 literals**: Applications using hardcoded IPv4 addresses (e.g. `connect("1.2.3.4")`) cannot use DNS64. CLAT on the client handles this for capable OSes; legacy apps on non-CLAT clients will fail on IPv6-only VLANs.
|
|
||||||
- **Native IPv6 bypassed**: `translate_all` means no traffic uses native IPv6 directly — everything goes through Tayga. This is intentional; it trades native IPv6 performance for a consistent exit IP. If native IPv6 is ever desired for specific destinations, remove `translate_all` and handle the HE captcha problem differently (e.g. per-domain exceptions).
|
|
||||||
- **IPv6-only destinations (no A record)**: With `translate_all`, the plugin still attempts an A lookup for every AAAA query. If no A record exists, `Synthesize` produces a NOERROR with an empty answer — the real AAAA is discarded. Confirmed by reading the source: `responseShouldDNS64` returns `true` unconditionally when `TranslateAll` is set (except NXDOMAIN), and `Synthesize` only converts A records — anything without an A record yields an empty answer. In practice this only affects genuinely IPv6-only destinations with no A record, which is rare on the public internet today.
|
|
||||||
+2
-11
@@ -93,12 +93,8 @@ There are also networks, which are not VLANs, but are routed:
|
|||||||
Static assignment on CRS, access to factory IP of ONU
|
Static assignment on CRS, access to factory IP of ONU
|
||||||
- Containers on CRS<br>
|
- Containers on CRS<br>
|
||||||
Access to every other network<br>
|
Access to every other network<br>
|
||||||
IP: 172.20.0.1/24, 2001:470:61a3:500::/64<br>
|
IP: 172.17.0.1/16, 2001:470:61a3:500::/64<br>
|
||||||
Static IP management, hosts Tailscale and CoreDNS (DNS64) containers
|
Static IP management
|
||||||
- NAT64 link on CRS<br>
|
|
||||||
Dedicated bridge for the Tayga NAT64 container<br>
|
|
||||||
IP: 192.168.239.0/30, fc64::/126 (link), 192.168.240.0/20 (Tayga dynamic pool)<br>
|
|
||||||
IPv6 traffic to 64:ff9b::/96 is routed here for translation to IPv4
|
|
||||||
|
|
||||||
Whole network is designed to eliminate VLANs, overlays where unnecessary to keep things simple. Only NAT rules are:
|
Whole network is designed to eliminate VLANs, overlays where unnecessary to keep things simple. Only NAT rules are:
|
||||||
|
|
||||||
@@ -107,13 +103,8 @@ Whole network is designed to eliminate VLANs, overlays where unnecessary to keep
|
|||||||
It doesn't have a gateway configured, we want to access it from other networks so we need to talk to it as if we were in the same subnet
|
It doesn't have a gateway configured, we want to access it from other networks so we need to talk to it as if we were in the same subnet
|
||||||
- src-nat tailscale IPv6 to internet<br>
|
- src-nat tailscale IPv6 to internet<br>
|
||||||
Tailscale assigns IPv6 from private subnet with no way to configure it, so the assigned IPs are not routable
|
Tailscale assigns IPv6 from private subnet with no way to configure it, so the assigned IPs are not routable
|
||||||
- Masquerade Tayga NAT64 dynamic pool (192.168.240.0/20) via GPON PPPoE
|
|
||||||
- IPv4 port forwards from GPON PPPoE to respective services
|
- IPv4 port forwards from GPON PPPoE to respective services
|
||||||
|
|
||||||
## IPv6-mostly (NAT64/DNS64)
|
|
||||||
|
|
||||||
LAN (vlan2) and IoT (vlan5) are IPv6-mostly networks (RFC 8925): clients capable of IPv6-only operation receive DHCP option 108, drop their IPv4 address, and activate CLAT using the NAT64 prefix advertised via PREF64 in router advertisements. Legacy clients keep dual-stack. DNS64 (CoreDNS container, with `translate_all`) synthesizes 64:ff9b::/96 AAAA answers so all named traffic exits via NAT64 (Tayga container) on our IPv4 WAN — bypassing the HE tunnel for egress and avoiding datacenter-IP captcha flagging. See [CoreDNS DNS64 + NAT64 design](./coredns-nat64.md) for details and deployment pitfalls.
|
|
||||||
|
|
||||||
There is also an UPnP and NAT-PMP enabled to automatically configure port forwards from LAN.
|
There is also an UPnP and NAT-PMP enabled to automatically configure port forwards from LAN.
|
||||||
|
|
||||||
## Uplink
|
## Uplink
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ spec:
|
|||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: cert-manager-webhook-ovh
|
chart: cert-manager-webhook-ovh
|
||||||
version: 0.9.13
|
version: 0.9.10
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: HelmRepository
|
kind: HelmRepository
|
||||||
name: cert-manager-webhook-ovh
|
name: cert-manager-webhook-ovh
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ spec:
|
|||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: cloudnative-pg
|
chart: cloudnative-pg
|
||||||
version: 0.28.3
|
version: 0.28.2
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: HelmRepository
|
kind: HelmRepository
|
||||||
name: cnpg
|
name: cnpg
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ spec:
|
|||||||
chart:
|
chart:
|
||||||
spec:
|
spec:
|
||||||
chart: openebs
|
chart: openebs
|
||||||
version: 4.5.0
|
version: 4.4.0
|
||||||
sourceRef:
|
sourceRef:
|
||||||
kind: HelmRepository
|
kind: HelmRepository
|
||||||
name: openebs
|
name: openebs
|
||||||
|
|||||||
@@ -1,147 +0,0 @@
|
|||||||
# Mikrotik containers
|
|
||||||
|
|
||||||
RouterOS containers running on the CRS418 providing network services that RouterOS cannot handle natively.
|
|
||||||
|
|
||||||
## CoreDNS (DNS64)
|
|
||||||
|
|
||||||
Replaces the built-in RouterOS DNS forwarder. Implements DNS64 (RFC 6147): synthesizes AAAA records from A records for IPv4-only destinations, enabling IPv6-only clients to reach them via NAT64. Native dual-stack sites keep their real AAAA records.
|
|
||||||
|
|
||||||
Source: [`coredns/`](coredns/)
|
|
||||||
Image built by Woodpecker CI, pushed to `gitea.lumpiasty.xyz/<owner>/coredns-mikrotik`.
|
|
||||||
|
|
||||||
### Why not the official coredns/coredns image?
|
|
||||||
|
|
||||||
The official image ships ~40 plugins and weighs ~20 MB compressed. A custom build with the 7 plugins we actually need fits in ~6-8 MB — important for the CRS internal flash.
|
|
||||||
|
|
||||||
### Corefile
|
|
||||||
|
|
||||||
The Corefile is baked into the image. To change DNS behaviour, edit [`coredns/Corefile`](coredns/Corefile) and push — the Woodpecker pipeline rebuilds and pushes a new image automatically.
|
|
||||||
|
|
||||||
## Tayga (NAT64)
|
|
||||||
|
|
||||||
Stateless NAT64 translator (RFC 7915). Receives IPv6 packets destined for `64:ff9b::/96`, rewrites them to IPv4, and returns translated responses. RouterOS does **not** implement NAT64 natively — the official docs state this explicitly.
|
|
||||||
|
|
||||||
Official image: `ghcr.io/apalrd/tayga` — no custom build needed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## RouterOS setup
|
|
||||||
|
|
||||||
The commands below wire both containers into the network. Adapt interface names and IPv6 prefix to your actual allocation. The HE tunnel broker prefix in use is `2001:470:61a3::/48`; the examples below use a dedicated /64 from the management range for container interfaces.
|
|
||||||
|
|
||||||
### 1. Enable container mode (one-time, requires physical access)
|
|
||||||
|
|
||||||
```
|
|
||||||
/system/device-mode/update container=yes
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Network interfaces
|
|
||||||
|
|
||||||
```
|
|
||||||
# CoreDNS — dedicated veth, no IPv6 needed (DNS listens on IPv4 of the veth)
|
|
||||||
/interface/veth/add name=veth-dns address=172.31.0.2/30 gateway=172.31.0.1
|
|
||||||
/interface/bridge/add name=br-dns
|
|
||||||
/interface/bridge/port/add bridge=br-dns interface=veth-dns
|
|
||||||
/ip/address/add address=172.31.0.1/30 interface=br-dns
|
|
||||||
|
|
||||||
# Tayga — needs both IPv4 (for its own address) and IPv6 (for the NAT64 traffic path)
|
|
||||||
/interface/veth/add name=veth-nat64 address=172.31.1.2/30 gateway=172.31.1.1
|
|
||||||
/interface/bridge/add name=br-nat64
|
|
||||||
/interface/bridge/port/add bridge=br-nat64 interface=veth-nat64
|
|
||||||
/ip/address/add address=172.31.1.1/30 interface=br-nat64
|
|
||||||
/ipv6/address/add address=2001:470:61a3:500::1/64 advertise=no interface=br-nat64
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. NAT for container internet access
|
|
||||||
|
|
||||||
```
|
|
||||||
/ip/firewall/nat/add chain=srcnat src-address=172.31.0.0/29 action=masquerade comment="container egress"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Tayga container
|
|
||||||
|
|
||||||
```
|
|
||||||
/container/config/set registry-url=https://ghcr.io tmpdir=flash/tmp
|
|
||||||
|
|
||||||
/container/envs/add list=ENV_TAYGA key=TAYGA_CONF_IPV4_ADDR value=172.31.1.2
|
|
||||||
/container/envs/add list=ENV_TAYGA key=TAYGA_CONF_DYNAMIC_POOL value=192.0.0.0/24
|
|
||||||
/container/envs/add list=ENV_TAYGA key=TAYGA_CONF_PREFIX value=64:ff9b::/96
|
|
||||||
/container/envs/add list=ENV_TAYGA key=TAYGA_IPV6_ADDR value=2001:470:61a3:500::2
|
|
||||||
|
|
||||||
/container/add \
|
|
||||||
remote-image=ghcr.io/apalrd/tayga:latest \
|
|
||||||
interface=veth-nat64 \
|
|
||||||
envlist=ENV_TAYGA \
|
|
||||||
root-dir=flash/tayga \
|
|
||||||
start-on-boot=yes \
|
|
||||||
logging=yes \
|
|
||||||
name=tayga
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. CoreDNS container
|
|
||||||
|
|
||||||
```
|
|
||||||
/container/config/set registry-url=https://gitea.lumpiasty.xyz
|
|
||||||
|
|
||||||
/container/add \
|
|
||||||
remote-image=gitea.lumpiasty.xyz/<owner>/coredns-mikrotik:latest \
|
|
||||||
interface=veth-dns \
|
|
||||||
root-dir=flash/coredns \
|
|
||||||
start-on-boot=yes \
|
|
||||||
logging=yes \
|
|
||||||
name=coredns
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Routes
|
|
||||||
|
|
||||||
```
|
|
||||||
# IPv6 traffic for the NAT64 prefix goes to Tayga
|
|
||||||
/ipv6/route/add dst-address=64:ff9b::/96 gateway=2001:470:61a3:500::2 comment="NAT64 via Tayga"
|
|
||||||
|
|
||||||
# IPv4 return traffic from Tayga's dynamic pool back to LAN clients
|
|
||||||
/ip/route/add dst-address=192.0.0.0/24 gateway=172.31.1.2 comment="Tayga dynamic pool"
|
|
||||||
|
|
||||||
# Masquerade Tayga's IPv4 pool to WAN
|
|
||||||
/ip/firewall/nat/add chain=srcnat src-address=192.0.0.0/24 action=masquerade comment="Tayga pool egress"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Point the router's DNS resolver at CoreDNS
|
|
||||||
|
|
||||||
```
|
|
||||||
/ip/dns/set servers=172.31.0.2 allow-remote-requests=yes
|
|
||||||
```
|
|
||||||
|
|
||||||
### 8. PREF64 in Router Advertisements
|
|
||||||
|
|
||||||
Tells CLAT-capable clients (iOS, Android, macOS) the NAT64 prefix without requiring DNS64 prefix discovery.
|
|
||||||
|
|
||||||
```
|
|
||||||
/ipv6/nd/set [find] pref64=64:ff9b::/96
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9. DHCP option 108 — IPv6-only preferred (RFC 8925)
|
|
||||||
|
|
||||||
Signals to capable clients that they may disable IPv4 and rely on CLAT/NAT64. Clients that don't understand option 108 ignore it and continue with dual-stack.
|
|
||||||
|
|
||||||
```
|
|
||||||
# 0x0000001c = 28 seconds — short for testing; use 0x00015180 (86400) in production
|
|
||||||
/ip/dhcp-server/option/add name=v6only-preferred code=108 value=0x0000001c
|
|
||||||
/ip/dhcp-server/option/sets/add name=v6only-set options=v6only-preferred
|
|
||||||
# Attach the option set to your DHCP server:
|
|
||||||
/ip/dhcp-server/set [find] dhcp-option-set=v6only-set
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verification
|
|
||||||
|
|
||||||
From a client on the LAN (IPv6-only or dual-stack):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Should return a synthesized 64:ff9b::/96 AAAA for an IPv4-only host
|
|
||||||
dig AAAA ipv4.google.com @172.31.0.2
|
|
||||||
|
|
||||||
# Should succeed — goes via NAT64
|
|
||||||
ping 64:ff9b::1.1.1.1
|
|
||||||
|
|
||||||
# Full path test from an IPv6-only client
|
|
||||||
curl -6 https://ipv4only.arpa/
|
|
||||||
```
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# Our own zone bypasses DNS64: internal services have native IPv6 (LB pool
|
|
||||||
# routed via HE prefix), so clients should get real AAAA records and connect
|
|
||||||
# directly instead of hairpinning through NAT64.
|
|
||||||
#
|
|
||||||
# This MUST live here, not as a RouterOS static FWD entry: RouterOS FWD
|
|
||||||
# entries return NOERROR with an empty answer instead of relaying NXDOMAIN,
|
|
||||||
# which breaks getaddrinfo search-domain processing (resolver stops at the
|
|
||||||
# first NODATA search candidate and never tries the absolute name -> apps
|
|
||||||
# fail with ENOTFOUND for names that exist).
|
|
||||||
lumpiasty.xyz:53 {
|
|
||||||
forward . 1.1.1.1 8.8.8.8 {
|
|
||||||
prefer_udp
|
|
||||||
}
|
|
||||||
|
|
||||||
cache 300
|
|
||||||
errors
|
|
||||||
log
|
|
||||||
}
|
|
||||||
|
|
||||||
.:53 {
|
|
||||||
# Synthesize AAAA from A records for all destinations.
|
|
||||||
# translate_all: override real AAAA records too, so all traffic exits
|
|
||||||
# via NAT64 (our IPv4 WAN) rather than the HE tunnel broker.
|
|
||||||
# This eliminates datacenter flagging and CAPTCHA loops from HE addresses.
|
|
||||||
dns64 {
|
|
||||||
prefix 64:ff9b::/96
|
|
||||||
translate_all
|
|
||||||
allow_ipv4
|
|
||||||
}
|
|
||||||
|
|
||||||
forward . 1.1.1.1 8.8.8.8 {
|
|
||||||
prefer_udp
|
|
||||||
}
|
|
||||||
|
|
||||||
cache 300
|
|
||||||
errors
|
|
||||||
log
|
|
||||||
reload
|
|
||||||
health :8080
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# Stage 1: build CoreDNS with minimal plugin set
|
|
||||||
FROM golang:1.25-alpine AS build
|
|
||||||
|
|
||||||
RUN apk add --no-cache git make bash
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
RUN git clone --depth 1 --branch v1.12.1 \
|
|
||||||
https://github.com/coredns/coredns .
|
|
||||||
|
|
||||||
# Overwrite plugin.cfg with our trimmed list before compilation
|
|
||||||
COPY plugin.cfg .
|
|
||||||
|
|
||||||
RUN go generate && make
|
|
||||||
|
|
||||||
# Stage 2: extract CA certificates from a full image
|
|
||||||
FROM debian:stable-slim AS certs
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
||||||
ca-certificates && rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Stage 3: minimal runtime — scratch + binary + certs only
|
|
||||||
FROM scratch
|
|
||||||
|
|
||||||
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
COPY --from=build /src/coredns /coredns
|
|
||||||
COPY Corefile /Corefile
|
|
||||||
|
|
||||||
# 53: DNS (UDP + TCP)
|
|
||||||
# 8080: health endpoint
|
|
||||||
EXPOSE 53/udp 53/tcp 8080/tcp
|
|
||||||
|
|
||||||
# RouterOS requires root to bind port 53 — no USER directive
|
|
||||||
ENTRYPOINT ["/coredns", "-conf", "/Corefile"]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
errors:errors
|
|
||||||
log:log
|
|
||||||
health:health
|
|
||||||
cache:cache
|
|
||||||
dns64:dns64
|
|
||||||
forward:forward
|
|
||||||
reload:reload
|
|
||||||
Reference in New Issue
Block a user