50 Commits

Author SHA1 Message Date
Renovate 6f7c4c91b4 Merge pull request 'Update Helm release cilium to v1.19.5' (#336) from renovate/cilium-1.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline failed
2026-06-17 02:01:25 +00:00
Renovate 4df35f496f Merge pull request 'Update alpine Docker tag to v3.24.1' (#335) from renovate/alpine-3.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline is pending
2026-06-17 02:01:10 +00:00
Renovate 8214677d03 Update Helm release cilium to v1.19.5 2026-06-17 02:01:09 +00:00
Renovate a01ec90b06 Update alpine Docker tag to v3.24.1 2026-06-17 02:01:07 +00:00
Lumpiasty d10c3efe68 increase gitea storage size
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-06-16 23:25:21 +02:00
Lumpiasty ae7f58240c Merge pull request 'Update golang Docker tag to v1.26' (#331) from renovate/golang-1.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/push/coredns-build Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
Reviewed-on: #331
2026-06-15 23:08:36 +00:00
Lumpiasty 9f5d45d515 Merge pull request 'Update renovate/renovate Docker tag to v43.222.1' (#333) from renovate/renovate-renovate-43.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was canceled
Reviewed-on: #333
2026-06-15 23:08:26 +00:00
Lumpiasty d3cb2a6e65 Decrease logging of CoreDNS
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/push/coredns-build Pipeline was successful
2026-06-16 00:53:56 +02:00
Lumpiasty 679ebb3465 Get rid of NAT64 setup
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/push/coredns-build Pipeline was successful
2026-06-16 00:29:45 +02:00
Renovate de10cba76c Update renovate/renovate Docker tag to v43.222.1 2026-06-15 02:01:26 +00:00
Renovate b993115b41 Merge pull request 'Update alpine/k8s Docker tag to v1.36.2' (#332) from renovate/alpine-k8s-1.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
2026-06-15 02:01:26 +00:00
Renovate f02579a2f2 Update alpine/k8s Docker tag to v1.36.2 2026-06-15 02:01:23 +00:00
Renovate e458949817 Merge pull request 'Update Helm release cert-manager-webhook-ovh to v0.9.13' (#330) from renovate/cert-manager-webhook-ovh-0.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-06-13 02:01:22 +00:00
Renovate 60ba0cfe90 Update golang Docker tag to v1.26
ci/woodpecker/push/coredns-build Pipeline was successful
2026-06-13 02:01:21 +00:00
Renovate cb5f459182 Update Helm release cert-manager-webhook-ovh to v0.9.13 2026-06-13 02:01:17 +00:00
Lumpiasty d3a067886e coredns: fix ENOTFOUND for own zone, enable dns64 for IPv4 clients
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/push/coredns-build Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
Two Corefile changes:
- Add lumpiasty.xyz server block without dns64. Replaces the manual
  RouterOS static FWD entry (\"bypass nat64\") which returned NOERROR
  with empty answer instead of relaying NXDOMAIN. Combined with
  ndots:5 and pod search domains this made getaddrinfo stop at the
  search-suffixed candidate and fail with ENOTFOUND for valid names
  (kaneo -> authentik OAuth fetch failures). CoreDNS relays rcodes
  faithfully; internal zone keeps real AAAA for native IPv6.
- Add allow_ipv4 to dns64 (previously uncommitted): without it only
  queries arriving over IPv6 are synthesized, but all clients reach
  CoreDNS via RouterOS over IPv4, so translate_all never applied.
The RouterOS static FWD entry must be removed after deploying the new
image - ansible already declares only the ts.net entry, so a playbook
run handles it.
2026-06-13 02:45:30 +02:00
Lumpiasty 33e01376b1 Add NAT64, DNS64 to network
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/push/coredns-build Pipeline failed
2026-06-13 00:27:43 +02:00
Lumpiasty 374ee146fe update llama-swap image
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
2026-06-12 18:17:52 +02:00
Lumpiasty 2380cd16e4 add more gemma 4 26b variants
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
2026-06-12 18:01:27 +02:00
Renovate 23ddd7c233 Merge pull request 'Update Helm release authentik to v2026.5.3' (#329) from renovate/authentik-2026.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
2026-06-12 02:01:25 +00:00
Renovate a6bfb3d93c Update Helm release authentik to v2026.5.3 2026-06-12 02:01:18 +00:00
Lumpiasty 59f32659a1 Merge pull request 'Update renovate/renovate Docker tag to v43.220.0' (#320) from renovate/renovate-renovate-43.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
Reviewed-on: #320
2026-06-11 21:47:26 +00:00
Lumpiasty 199b14b810 Merge pull request 'Update Helm release openebs to v4.5.0' (#322) from renovate/openebs-4.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was canceled
Reviewed-on: #322
2026-06-11 21:47:07 +00:00
Lumpiasty fc971e6e6c Merge pull request 'Update alpine Docker tag to v3.24.0' (#327) from renovate/alpine-3.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was canceled
Reviewed-on: #327
2026-06-11 21:47:00 +00:00
Lumpiasty aab4bc279c Merge pull request 'Update ghcr.io/remsky/kokoro-fastapi-cpu Docker tag to v0.5.0' (#324) from renovate/ghcr.io-remsky-kokoro-fastapi-cpu-0.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was canceled
Reviewed-on: #324
2026-06-11 21:46:39 +00:00
Lumpiasty 7f6439d64a switch gemma 4 quant, add mtp and nothink variants
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
2026-06-11 21:06:12 +02:00
Renovate f0921e903a Update renovate/renovate Docker tag to v43.220.0 2026-06-11 02:01:22 +00:00
Renovate 5fed73515b Merge pull request 'Update Helm release cloudnative-pg to v0.28.3' (#328) from renovate/cloudnative-pg-0.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
2026-06-11 02:01:17 +00:00
Renovate 1c092c8044 Update Helm release cloudnative-pg to v0.28.3 2026-06-11 02:01:13 +00:00
Lumpiasty 8860f6782e add converting proxy to parakeet
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-06-10 20:46:17 +02:00
Lumpiasty f863a0a496 use parakeet.cpp instead of whisper
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
2026-06-10 20:08:14 +02:00
Renovate 6bf31f0ae6 Update alpine Docker tag to v3.24.0 2026-06-10 02:01:07 +00:00
Renovate 979f5796d5 Merge pull request 'Update Helm release cert-manager-webhook-ovh to v0.9.11' (#326) from renovate/cert-manager-webhook-ovh-0.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-06-09 15:42:31 +00:00
Renovate 8bca1cf90f Merge pull request 'Update ghcr.io/mostlygeek/llama-swap Docker tag to unified-vulkan-2026-06-09' (#325) from renovate/ghcr.io-mostlygeek-llama-swap-2026.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline is pending
2026-06-09 15:42:26 +00:00
Renovate b3793d11d9 Update Helm release cert-manager-webhook-ovh to v0.9.11 2026-06-09 15:42:26 +00:00
Renovate fb4fa9b0e7 Update ghcr.io/mostlygeek/llama-swap Docker tag to unified-vulkan-2026-06-09 2026-06-09 15:40:20 +00:00
Renovate 13a87e5b00 Update ghcr.io/remsky/kokoro-fastapi-cpu Docker tag to v0.5.0 2026-06-07 02:02:49 +00:00
Renovate 1da43d39e2 Merge pull request 'Update ghcr.io/mostlygeek/llama-swap Docker tag to unified-vulkan-2026-06-06' (#323) from renovate/ghcr.io-mostlygeek-llama-swap-2026.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-06-07 02:02:49 +00:00
Renovate 87c56a9ca1 Update ghcr.io/mostlygeek/llama-swap Docker tag to unified-vulkan-2026-06-06 2026-06-07 02:01:15 +00:00
Renovate 8ff9126025 Merge pull request 'Update ghcr.io/mostlygeek/llama-swap Docker tag to unified-vulkan-2026-06-05' (#321) from renovate/ghcr.io-mostlygeek-llama-swap-2026.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-06-06 02:02:46 +00:00
Renovate 4e0a97d6f8 Update Helm release openebs to v4.5.0 2026-06-06 02:02:45 +00:00
Renovate 43c2036642 Update ghcr.io/mostlygeek/llama-swap Docker tag to unified-vulkan-2026-06-05 2026-06-06 02:01:10 +00:00
Renovate 4d51d45f74 Merge pull request 'Update ghcr.io/mostlygeek/llama-swap Docker tag to unified-vulkan-2026-06-04' (#319) from renovate/ghcr.io-mostlygeek-llama-swap-2026.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-06-05 02:02:58 +00:00
Renovate fe607d3fb8 Update ghcr.io/mostlygeek/llama-swap Docker tag to unified-vulkan-2026-06-04 2026-06-05 02:01:18 +00:00
Lumpiasty cd514c71b6 oauth auto redirect and allowed role in openwebui
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-06-04 18:03:00 +02:00
Lumpiasty 32a483c711 update outdated readme sections 2026-06-04 17:24:17 +02:00
Lumpiasty 0426f86719 Merge pull request 'Update renovate/renovate Docker tag to v43.210.2' (#314) from renovate/renovate-renovate-43.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was successful
Reviewed-on: #314
2026-06-04 14:40:48 +00:00
Lumpiasty da365501e9 Merge pull request 'Update Helm release open-webui to v14.8.0' (#315) from renovate/open-webui-14.x into fresh-start
ci/woodpecker/push/flux-reconcile-source Pipeline was canceled
Reviewed-on: #315
2026-06-04 14:40:42 +00:00
Renovate cf7c0075e7 Update renovate/renovate Docker tag to v43.210.2 2026-06-04 02:03:00 +00:00
Renovate 973b0beb21 Update Helm release open-webui to v14.8.0 2026-06-03 02:03:19 +00:00
28 changed files with 962 additions and 76 deletions
+46
View File
@@ -0,0 +1,46 @@
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]
+1 -1
View File
@@ -20,7 +20,7 @@ steps:
- export VAULT_TOKEN=$(cat /woodpecker/.vault_id) - export VAULT_TOKEN=$(cat /woodpecker/.vault_id)
- bao write -format json -f /kubernetes/creds/flux-reconcile > /woodpecker/kube_credentials - bao write -format json -f /kubernetes/creds/flux-reconcile > /woodpecker/kube_credentials
- name: Construct Kubeconfig - name: Construct Kubeconfig
image: alpine/k8s:1.36.1 image: alpine/k8s:1.36.2
environment: environment:
KUBECONFIG: /woodpecker/kubeconfig KUBECONFIG: /woodpecker/kubeconfig
commands: commands:
+1 -1
View File
@@ -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.197.0 image: renovate/renovate:43.222.1
environment: environment:
RENOVATE_AUTODISCOVER: "true" RENOVATE_AUTODISCOVER: "true"
RENOVATE_ENDPOINT: https://gitea.lumpiasty.xyz/api/v1 RENOVATE_ENDPOINT: https://gitea.lumpiasty.xyz/api/v1
+3 -1
View File
@@ -139,6 +139,8 @@ 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
@@ -269,7 +271,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. You can speed up this process by "notifying" Flux using `flux reconcile source git flux-system`. 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.
Flux watches 3 kustomizations in this repo: Flux watches 3 kustomizations in this repo:
+6 -4
View File
@@ -40,16 +40,18 @@
- address: 2001:470:70:dd::2/64 - address: 2001:470:70:dd::2/64
advertise: false advertise: false
interface: sit1 interface: sit1
- address: ::ffff:ffff:ffff:ffff/64 # Static instead of from-pool: pool allocation is dynamic (first free /64,
from-pool: pool1 # e.g. ...:0::/64) which made the RDNSS address advertised in ND config
# 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: containers
- address: 2001:470:61a3:100::1/64 - address: 2001:470:61a3:100::1/64
advertise: false advertise: false
interface: vlan4 interface: vlan4
- address: ::ffff:ffff:ffff:ffff/64 - address: 2001:470:61a3:a: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
+49 -19
View File
@@ -65,6 +65,9 @@
- bridge: containers - bridge: containers
interface: veth-tailscale interface: veth-tailscale
comment: Tailscale container interface comment: Tailscale container interface
- bridge: containers
interface: veth-coredns
comment: CoreDNS container interface
- bridge: bridge1 - bridge: bridge1
interface: ether1 interface: ether1
pvid: 2 pvid: 2
@@ -152,24 +155,9 @@
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 DHCP networks # Pool is no longer referenced — vlan2/vlan5 now use static addresses
community.routeros.api_modify: # (addressing.yml) so the RDNSS addresses in ND config are deterministic.
path: ip dhcp-server network # Kept defined for one run after migration; safe to delete afterwards.
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
@@ -188,7 +176,9 @@
values: values:
allow-remote-requests: true allow-remote-requests: true
cache-size: 20480 cache-size: 20480
servers: 1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001 # CoreDNS container: plain forwarder with selective AAAA suppression.
# Forwards upstream to 1.1.1.1/8.8.8.8.
servers: 172.20.0.3
- name: Configure DNS static entries - name: Configure DNS static entries
community.routeros.api_modify: community.routeros.api_modify:
@@ -199,6 +189,12 @@
forward-to: 100.100.100.100 forward-to: 100.100.100.100
match-subdomain: true match-subdomain: true
comment: Tailscale MagicDNS 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). Our own zone is handled in the CoreDNS Corefile (lumpiasty.xyz
# server block, AAAA kept) which relays rcodes correctly.
# See docs/coredns.md.
handle_absent_entries: remove handle_absent_entries: remove
handle_entries_content: remove_as_much_as_possible handle_entries_content: remove_as_much_as_possible
@@ -244,6 +240,22 @@
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 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
- 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
- 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
@@ -252,3 +264,21 @@
default: true default: true
values: values:
advertise-dns: true advertise-dns: true
# RDNSS (RFC 8106): advertise an IPv6 DNS server in RAs so dual-stack clients
# have an IPv6 resolver. Points at the router's per-VLAN IPv6 address; RouterOS
# DNS forwards to CoreDNS. No pref64 — NAT64 has been removed (see docs/coredns.md);
# AAAA suppression now happens in CoreDNS, no client-side translation needed.
- name: Configure IPv6 ND per-interface (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
dns: 2001:470:61a3:9:ffff:ffff:ffff:ffff
- interface: vlan5
advertise-dns: true
dns: 2001:470:61a3:a:ffff:ffff:ffff:ffff
+10 -3
View File
@@ -20,15 +20,15 @@
data: data:
- dst: /var/lib/tailscale - dst: /var/lib/tailscale
list: tailscale_state list: tailscale_state
src: tailscale/state src: /tailscale/state
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 tailscale container - name: Configure containers
community.routeros.api_modify: community.routeros.api_modify:
path: container path: container
data: data:
- dns: 172.17.0.1 - dns: 172.20.0.1
interface: veth-tailscale interface: veth-tailscale
logging: true logging: true
mountlists: tailscale_state mountlists: tailscale_state
@@ -36,5 +36,12 @@
remote-image: gitea.lumpiasty.xyz/lumpiasty/mikrotik-tailscale:stable remote-image: gitea.lumpiasty.xyz/lumpiasty/mikrotik-tailscale:stable
root-dir: tailscale/root root-dir: tailscale/root
start-on-boot: true start-on-boot: true
- dns: 172.20.0.1
interface: veth-coredns
logging: true
name: coredns
remote-image: gitea.lumpiasty.xyz/lumpiasty/coredns-mikrotik:latest
root-dir: coredns/root
start-on-boot: true
handle_absent_entries: remove handle_absent_entries: remove
handle_entries_content: remove_as_much_as_possible handle_entries_content: remove_as_much_as_possible
+7 -1
View File
@@ -8,7 +8,8 @@
keepalive-timeout: 2 keepalive-timeout: 2
name: pppoe-gpon name: pppoe-gpon
password: "{{ routeros_pppoe_password }}" password: "{{ routeros_pppoe_password }}"
use-peer-dns: true # Using CoreDNS container with DNS64
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
@@ -37,5 +38,10 @@
mac-address: 7E:7E:A1:B1:2A:7B mac-address: 7E:7E:A1:B1:2A:7B
name: veth-tailscale name: veth-tailscale
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
handle_absent_entries: remove handle_absent_entries: remove
handle_entries_content: remove_as_much_as_possible handle_entries_content: remove_as_much_as_possible
+1 -1
View File
@@ -18,7 +18,7 @@ spec:
chart: chart:
spec: spec:
chart: authentik chart: authentik
version: 2026.5.2 version: 2026.5.3
sourceRef: sourceRef:
kind: HelmRepository kind: HelmRepository
name: authentik name: authentik
+3 -3
View File
@@ -7,7 +7,7 @@ metadata:
name: gitea-shared-storage-lvmhdd name: gitea-shared-storage-lvmhdd
namespace: openebs namespace: openebs
spec: spec:
capacity: 10Gi capacity: "21474836480"
ownerNodeID: anapistula-delrosalae ownerNodeID: anapistula-delrosalae
shared: "yes" shared: "yes"
thinProvision: "no" thinProvision: "no"
@@ -20,7 +20,7 @@ metadata:
name: gitea-shared-storage-lvmhdd name: gitea-shared-storage-lvmhdd
spec: spec:
capacity: capacity:
storage: 10Gi storage: 20Gi
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
persistentVolumeReclaimPolicy: Retain persistentVolumeReclaimPolicy: Retain
@@ -41,6 +41,6 @@ spec:
- ReadWriteOnce - ReadWriteOnce
resources: resources:
requests: requests:
storage: 10Gi storage: 20Gi
storageClassName: hdd-lvmpv storageClassName: hdd-lvmpv
volumeName: gitea-shared-storage-lvmhdd volumeName: gitea-shared-storage-lvmhdd
+199 -27
View File
@@ -14,12 +14,13 @@ 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"
- "whisper-small" - "parakeet-tdt_ctc-1.1b"
# 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.
@@ -27,7 +28,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: "whisper-small" stt: "parakeet-tdt_ctc-1.1b"
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"
@@ -38,10 +39,24 @@ 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:UD-Q4_K_XL" g26xl: "gemma-4-26B-A4B-it-qat:UD-Q4_K_XL"
g26q2: "gemma-4-26B-A4B-it:UD-Q2_K_XL" g26xlnt: "gemma-4-26B-A4B-it-qat-nothink:UD-Q4_K_XL"
ge4xl: "unsloth/gemma-4-E4B-it-GGUF:UD-Q4_K_XL" g26mtp: "gemma-4-26B-A4B-it-qat-mtp:UD-Q4_K_XL"
ge2xl: "unsloth/gemma-4-E2B-it-GGUF:UD-Q4_K_XL" g26mtpnt: "gemma-4-26B-A4B-it-qat-mtp-nothink: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"
@@ -54,10 +69,11 @@ 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 | g26q2 | ge4xl | ge2xl | q36t | q36nt | haut | haunt | mtpt | mtpnt) & q8 & stt" 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"
# 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: |
@@ -151,38 +167,200 @@ models:
${qwen35_nothink_args} ${qwen35_nothink_args}
${common_args} ${common_args}
"gemma-4-26B-A4B-it:UD-Q4_K_XL": "gemma-4-26B-A4B-it-qat:UD-Q4_K_XL":
cmd: | cmd: |
llama-server llama-server
-hf unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_XL \ -hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:UD-Q4_K_XL \
${ctx_256k} ${ctx_256k}
${gemma4_sampling} ${gemma4_sampling}
${common_args} ${common_args}
"gemma-4-26B-A4B-it:UD-Q2_K_XL": "gemma-4-26B-A4B-it-qat-nothink:UD-Q4_K_XL":
cmd: | cmd: |
llama-server llama-server
-hf unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q2_K_XL \ -hf unsloth/gemma-4-26B-A4B-it-qat-GGUF:UD-Q4_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}
"unsloth/gemma-4-E4B-it-GGUF:UD-Q4_K_XL": "gemma-4-26B-A4B-it-qat-mtp-nothink:UD-Q4_K_XL":
cmd: | cmd: |
llama-server llama-server
-hf unsloth/gemma-4-E4B-it-GGUF:UD-Q4_K_XL \ -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}
${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-E2B-it-GGUF:UD-Q4_K_XL": "unsloth/gemma-4-E4B-it-qat-GGUF-nothink:UD-Q4_K_XL":
cmd: | cmd: |
llama-server llama-server
-hf unsloth/gemma-4-E2B-it-GGUF:UD-Q4_K_XL \ -hf unsloth/gemma-4-E4B-it-qat-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
@@ -235,20 +413,14 @@ models:
--parallel 1 --parallel 1
${common_args} ${common_args}
# STT via whisper.cpp (Vulkan GPU on RX 580, always loaded, ~600MB VRAM) # STT via parakeet-server (parakeet.cpp OpenAI-compatible server, CPU, always loaded)
# Model auto-downloaded by init container, see deployment.yaml # Model downloaded on first start and cached under /root/.cache/parakeet.cpp/models
# Note: Vulkan whisper on AMD GPUs has known quality issues on some cards; # parakeet-proxy.py sits in front to convert any audio format to WAV via ffmpeg,
# if transcriptions come out as garbage/gibberish, add --no-gpu to fall back. # since parakeet-server only accepts real WAV but browsers send Ogg/Opus.
"whisper-small": "parakeet-tdt_ctc-1.1b":
checkEndpoint: none checkEndpoint: none
cmd: | cmd: |
whisper-server env PROXY_PORT=${PORT} FFMPEG_BIN=/root/.cache/ffmpeg/ffmpeg python3 /config/parakeet-proxy.py
--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)
+227
View File
@@ -0,0 +1,227 @@
#!/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()
+2 -2
View File
@@ -18,7 +18,7 @@ spec:
spec: spec:
initContainers: initContainers:
- name: download-whisper - name: download-whisper
image: ghcr.io/mostlygeek/llama-swap:unified-vulkan-2026-06-03 image: gitea.lumpiasty.xyz/lumpiasty/llama-swap:unified-vulkan-parakeet-2026-06-12
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: ghcr.io/mostlygeek/llama-swap:unified-vulkan-2026-06-03 image: gitea.lumpiasty.xyz/lumpiasty/llama-swap:unified-vulkan-parakeet-2026-06-12
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- llama-swap - llama-swap
+1 -1
View File
@@ -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.4.0 image: ghcr.io/remsky/kokoro-fastapi-cpu:v0.5.0
ports: ports:
- containerPort: 8880 - containerPort: 8880
name: http name: http
+1
View File
@@ -13,3 +13,4 @@ configMapGenerator:
namespace: llama namespace: llama
files: files:
- config.yaml=configs/config.yaml - config.yaml=configs/config.yaml
- parakeet-proxy.py=configs/parakeet-proxy.py
+1 -1
View File
@@ -15,7 +15,7 @@ spec:
spec: spec:
initContainers: initContainers:
- name: prepare-home - name: prepare-home
image: alpine:3.23.4 image: alpine:3.24.1
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- /bin/sh - /bin/sh
+13 -5
View File
@@ -18,7 +18,7 @@ spec:
chart: chart:
spec: spec:
chart: open-webui chart: open-webui
version: 14.6.0 version: 14.8.0
sourceRef: sourceRef:
kind: HelmRepository kind: HelmRepository
name: open-webui name: open-webui
@@ -74,7 +74,17 @@ spec:
value: "false" value: "false"
- name: OAUTH_MERGE_ACCOUNTS_BY_EMAIL - name: OAUTH_MERGE_ACCOUNTS_BY_EMAIL
value: "true" value: "true"
# STT via whisper-server (routed through llama-swap) - name: ENABLE_OAUTH_ROLE_MANAGEMENT
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
@@ -82,9 +92,7 @@ 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: "whisper-small" value: "parakeet-tdt_ctc-1.1b"
- 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
+110
View File
@@ -0,0 +1,110 @@
# CoreDNS resolver
## Goal
Replace the RouterOS built-in DNS forwarder with a CoreDNS container for
configurability, and suppress IPv6 (AAAA) resolution by default to keep traffic
on IPv4.
## Background
The ISP provides no native IPv6 — only a Hurricane Electric (HE) tunnel
(`2001:470:61a3::/48`). HE addresses fall in ranges some sites flag as
datacenter/bot traffic, producing endless CAPTCHAs. The goal is to prefer IPv4
egress while keeping IPv6 available for our own services and any domain
explicitly trusted over IPv6.
## What this is NOT (and why)
An earlier iteration used **DNS64 + NAT64 (Tayga)** to force traffic through
IPv4. It was removed:
- **Performance**: Tayga is a userspace translator with no hardware offload.
Every translated packet crossed RouterOS twice (v6 in, v4 out) plus a
userspace hop, capping throughput at ~250 Mbps against a 1 Gbps line.
- **SPOF**: two containers (CoreDNS + Tayga) in the datapath of nearly all
traffic on a router whose native forwarder had been rock-solid.
- **Architectural inversion**: NAT64 exists to let IPv6-only clients reach IPv4.
We don't want IPv6 egress at all — using NAT64 to avoid IPv6 was solving the
problem backwards.
Plain AAAA suppression in CoreDNS achieves the same IPv4-preferred outcome with
zero datapath overhead — DNS is the only thing touched, packet forwarding stays
on the RouterOS fastpath at line rate.
The full account of the NAT64/IPv6-mostly attempt and why it was abandoned is in
[nat64-dns64-postmortem.md](./nat64-dns64-postmortem.md).
## How it works
CoreDNS runs as a single container (`172.20.0.3`), reachable from RouterOS DNS
which forwards client queries to it. The [Corefile](../mikrotik/coredns/Corefile)
has three server blocks:
1. **`lumpiasty.xyz`** — our own zone. Forwards normally, keeps AAAA, so internal
services reachable over the HE prefix resolve to their real IPv6 addresses.
2. **`.` (default)** — forwards everything else, but a `template IN AAAA` block
returns empty NOERROR for all AAAA queries, so clients fall back to IPv4 and
avoid the HE tunnel's flagged egress. A records and all other types pass
through untouched.
The whitelist is implemented as a reusable `(aaaa_allowed)` snippet imported by
zones that should keep AAAA. To trust another domain over IPv6, add a server
block for it that imports `aaaa_allowed`.
### Why suppression, not NXDOMAIN
The AAAA template returns NOERROR with an empty answer (NODATA), not NXDOMAIN.
This is correct: the name exists, it just has no (advertised) AAAA. Clients
treat it as "no IPv6 address" and use the A record. Returning NXDOMAIN would
wrongly imply the name doesn't exist and break the A lookup.
## Future improvement
The current global-suppress-plus-whitelist is coarse: a domain that is genuinely
IPv6-only (no A record) and not whitelisted becomes unreachable. The intended
end state is a plugin that suppresses AAAA only when the domain also has an A
record, so IPv6-only destinations keep working without manual whitelisting. No
in-tree CoreDNS plugin does this today.
## Custom image
Built from source with a minimal plugin set (`errors`, `log`, `health`,
`template`, `cache`, `forward`, `reload`) instead of the default ~40, producing
a ~6-8 MB image. The `dns64` plugin is no longer compiled in.
Source: [`mikrotik/coredns/`](../mikrotik/coredns/). Built by Woodpecker
([`.woodpecker/coredns-build.yaml`](../.woodpecker/coredns-build.yaml)) on pushes
touching `mikrotik/coredns/**`, pushed to `gitea.lumpiasty.xyz/lumpiasty/coredns-mikrotik`.
## RouterOS integration
- `/ip/dns servers=172.20.0.3` — RouterOS forwards client queries to CoreDNS
- RDNSS in RA (`/ipv6/nd dns=...` on vlan2/vlan5) advertises an IPv6 resolver
(the router's per-VLAN address) to dual-stack clients; RouterOS DNS relays to
CoreDNS
- No DHCP option 108, no PREF64 — those belonged to the removed IPv6-mostly setup
## Pitfalls learned (kept for reference)
These were hit during the NAT64 era and the migration; some still apply:
1. **RouterOS static FWD entries corrupt NXDOMAIN.** A `type=FWD match-subdomain=yes`
entry returns NOERROR/empty instead of relaying NXDOMAIN. Combined with
`ndots:5` and kubernetes pod search domains, `getaddrinfo` stops at the first
search-suffixed NODATA candidate and never tries the absolute name — apps fail
with `ENOTFOUND` for valid hostnames while `nslookup` (absolute query) works.
Our own zone is therefore handled in the Corefile, not via a RouterOS FWD
entry. RouterOS DNS does plain forwarding only (plus the Tailscale `ts.net`
FWD, which is acceptable as its subdomains genuinely don't exist publicly).
2. **`advertise-dns=no` on new ND entries.** RouterOS creates per-interface
`ipv6 nd` entries with `advertise-dns=no`, suppressing the RDNSS option even
when a static `dns=` list is set. Must be enabled explicitly.
3. **Per-interface ND entries must be created, not modified.** Only the
`interface=all` default ships out of the box; `api_find_and_modify` matching a
specific interface silently matches nothing. Use `api_modify`.
Verification: `rdisc6` (NixOS package `ndisc6`) dumps RA contents. The CoreDNS
`log` plugin output is visible via `/log print` on the router (container
`logging=yes`) and shows the rcode CoreDNS returned — comparing it to what the
client received isolates which hop corrupts a response.
+136
View File
@@ -0,0 +1,136 @@
# Postmortem: NAT64 / IPv6-mostly attempt
A record of an architecture that was built, run for ~2 days, and removed. Kept
so the reasoning isn't re-discovered the hard way. For the current DNS setup see
[coredns.md](./coredns.md); for network overview see [network.md](./network.md).
## The original problem
The ISP provides no native IPv6 — only a Hurricane Electric (HE) 6in4 tunnel
(`2001:470:61a3::/48`). HE address ranges are widely classified as
datacenter/hosting space, so some sites (Google, Cloudflare-fronted services,
various login flows) treat IPv6 traffic from them as bot/VPN traffic: endless
CAPTCHAs, "unusual traffic" interstitials, or outright blocks. IPv4 egress
(the ISP's residential PPPoE address) is unaffected.
The goal: keep using the network normally without IPv6 triggering these flags,
while still wanting some IPv6 (e.g. inbound to self-hosted services).
## What was built
An **IPv6-mostly** network (RFC 8925) with **DNS64 + NAT64**, intended to push
egress onto IPv4 while presenting IPv6 to clients:
- **CoreDNS container** with the `dns64` plugin (`translate_all`): synthesized
`64:ff9b::/96` AAAA records from A records for *all* names, so even dual-stack
destinations resolved to a NAT64 address.
- **Tayga container** (`ghcr.io/apalrd/tayga-nat64`): stateless NAT64 translator.
IPv6 traffic to `64:ff9b::/96` was routed to it, translated to IPv4, and
masqueraded out the GPON PPPoE interface. So all "IPv6" egress actually left
as IPv4 on the residential address — bypassing the HE tunnel and its flagging.
- **RouterOS RA + DHCP**: DHCP option 108 (IPv6-only preferred) to make capable
clients drop IPv4, PREF64 (RFC 8781) to advertise the NAT64 prefix for CLAT,
RDNSS (RFC 8106) to hand IPv6-only clients a resolver.
- Dedicated `nat64` bridge, `fc64::/126` link, `192.168.240.0/20` Tayga pool,
static routes, and firewall rules (including NAT64-mapped RFC1918 blocks to
prevent the translator being used as a policy bypass).
## Why it was removed
### 1. Performance — the dealbreaker
Throughput collapsed from line rate (~1 Gbps) to **~200-300 Mbps**, saturating
the router CPU. Causes, all structural:
- Tayga is a **userspace** translator. Every translated packet leaves the kernel
fastpath, is copied to userspace, translated, and re-injected.
- Translated traffic crosses RouterOS **twice** — once as IPv6 (LAN → Tayga),
once as IPv4 (Tayga → WAN, with masquerade) — doubling firewall/conntrack work.
- No hardware offload or fasttrack applies to either leg.
With `translate_all`, *nearly all* internet traffic went through this path, so
the penalty hit everything, not just IPv4-only destinations.
### 2. Single point of failure
DNS (CoreDNS) and most of the datapath (Tayga) became two containers in the
critical path on a router whose built-in forwarder had been completely reliable.
Container restarts, image pulls, or a crash now took down connectivity.
### 3. Architectural inversion
NAT64 exists to let **IPv6-only** clients reach the **IPv4** internet. The actual
goal here was the opposite — *avoid* IPv6 egress entirely. Building an IPv6-only
client environment (option 108, CLAT, PREF64) and then translating all of it back
to IPv4 was solving the problem backwards. The complexity existed only to route
around a property of the HE tunnel.
### 4. Firewall complexity and a translation bypass hole
NAT64 punched a hole in the firewall model. RouterOS filters IPv4 and IPv6
independently, but NAT64 traffic enters as IPv6 and *leaves* as IPv4 after
translation — so the carefully-built IPv4 forward policy (inter-VLAN isolation,
RFC1918-to-WAN blocks) was simply bypassed for anything arriving via the
translator. A client could reach a private IPv4 range by encoding it in the
NAT64 prefix (`64:ff9b::c0a8:xxyy` = `192.168.x.y`), and the IPv4 rules would
never see it because the packet was IPv6 until Tayga rewrote it.
Plugging this required mirroring the IPv4 policy in the IPv6 chain: explicit
`reject` rules for every NAT64-mapped RFC1918 block (`64:ff9b::a00:0/104`,
`64:ff9b::ac10:0/108`, `64:ff9b::c0a8:0/112`), per-VLAN accept rules toward the
`nat64` interface, plus a separate masquerade and LB hairpin-accept for the
Tayga pool. That is a parallel, easy-to-get-wrong copy of the existing ruleset,
whose correctness depended on getting CIDR-to-prefix arithmetic right. Removing
NAT64 deleted all of it.
### 5. Operational fragility (see coredns.md for detail)
The setup had a long tail of subtle failure modes, each presenting identically
as "client can't connect":
- RouterOS static `FWD` entries return `NOERROR`/empty instead of relaying
`NXDOMAIN`, which broke `getaddrinfo` search-domain handling in Kubernetes
pods (`ENOTFOUND` for valid names).
- `translate_all` discarded real AAAA for IPv6-only internal services, and
returned empty answers for names with no A record.
- Per-interface RouterOS `ipv6 nd` entries default to `advertise-dns=no` and must
be *created* (not modified), so RDNSS/PREF64 silently never advertised.
- Dynamic `from-pool` VLAN addressing made advertised RDNSS addresses point at
nonexistent router addresses.
- Option 108 honoured by clients before the NAT64 path was verified working left
them stuck "obtaining IP address".
Each was individually fixable, but the aggregate was a brittle system whose
benefit didn't justify the surface area.
## What replaced it
Plain CoreDNS forwarder with **AAAA suppression by default** plus a whitelist for
domains that should keep IPv6 (our own zone over the HE prefix, and any explicitly
trusted domain). Clients prefer IPv4 because they simply don't receive AAAA for
most names — no translation, no extra datapath hop, packet forwarding stays on the
RouterOS fastpath at line rate. DNS is the only thing in the path. See
[coredns.md](./coredns.md).
Tradeoff accepted: a non-whitelisted IPv6-only destination (no A record) is
unreachable. In practice essentially everything on the public internet still has
an A record. The intended future refinement is a CoreDNS plugin that suppresses
AAAA only when an A record also exists, removing the need for the whitelist; no
in-tree plugin does this today.
## Lessons
- **Measure throughput before committing to an in-path translator on SOHO-class
hardware.** Userspace NAT64 (Tayga/Jool-in-container) on a MikroTik CPU is
fine for a few hundred Mbps, not for saturating a gigabit line.
- **Match the mechanism to the actual goal.** The goal was "prefer IPv4 egress",
which is a one-line DNS policy, not a transition technology.
- **Prefer solutions that stay on the fastpath.** Anything that pulls bulk
traffic into userspace or doubles the forwarding work will dominate the CPU.
- **Fewer moving parts in the critical path.** Two containers carrying all DNS
and most traffic is a worse availability story than the stock forwarder, for a
cosmetic benefit (avoiding CAPTCHAs on some sites).
- **Protocol translation breaks the firewall model.** When traffic changes L3
protocol mid-path, the two firewall policies must be kept in sync by hand, and
any gap is a silent bypass. A solution that doesn't translate keeps a single
coherent policy.
+6 -2
View File
@@ -93,8 +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.17.0.1/16, 2001:470:61a3:500::/64<br> IP: 172.20.0.1/24, 2001:470:61a3:500::/64<br>
Static IP management Static IP management, hosts Tailscale and CoreDNS containers
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:
@@ -105,6 +105,10 @@ Whole network is designed to eliminate VLANs, overlays where unnecessary to keep
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
- IPv4 port forwards from GPON PPPoE to respective services - IPv4 port forwards from GPON PPPoE to respective services
## DNS and IPv6 preference
DNS is served by a CoreDNS container (`172.20.0.3`); RouterOS forwards client queries to it. CoreDNS suppresses AAAA records by default so clients prefer IPv4, avoiding the HE tunnel's datacenter-flagged egress (which triggers CAPTCHAs on some sites). Our own zone (`lumpiasty.xyz`) and any explicitly whitelisted domains keep AAAA for native IPv6. See [CoreDNS resolver](./coredns.md). An earlier NAT64/IPv6-mostly approach to the same problem was built and abandoned; see the [postmortem](./nat64-dns64-postmortem.md).
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.10 version: 0.9.13
sourceRef: sourceRef:
kind: HelmRepository kind: HelmRepository
name: cert-manager-webhook-ovh name: cert-manager-webhook-ovh
+1 -1
View File
@@ -23,7 +23,7 @@ spec:
chart: chart:
spec: spec:
chart: cilium chart: cilium
version: 1.19.4 version: 1.19.5
sourceRef: sourceRef:
kind: HelmRepository kind: HelmRepository
name: cilium name: cilium
+1 -1
View File
@@ -23,7 +23,7 @@ spec:
chart: chart:
spec: spec:
chart: cloudnative-pg chart: cloudnative-pg
version: 0.28.2 version: 0.28.3
sourceRef: sourceRef:
kind: HelmRepository kind: HelmRepository
name: cnpg name: cnpg
+1 -1
View File
@@ -23,7 +23,7 @@ spec:
chart: chart:
spec: spec:
chart: openebs chart: openebs
version: 4.4.0 version: 4.5.0
sourceRef: sourceRef:
kind: HelmRepository kind: HelmRepository
name: openebs name: openebs
+43
View File
@@ -0,0 +1,43 @@
# Mikrotik containers
RouterOS containers running on the CRS418 providing network services that
RouterOS cannot handle natively.
## CoreDNS
Replaces the built-in RouterOS DNS forwarder. Plain forwarding resolver with
selective AAAA suppression: AAAA is suppressed by default so clients prefer IPv4
(avoiding the HE tunnel's datacenter-flagged egress), while our own zone and any
whitelisted domains keep AAAA for native IPv6.
Source: [`coredns/`](coredns/). Image built by Woodpecker CI
([`.woodpecker/coredns-build.yaml`](../.woodpecker/coredns-build.yaml)), pushed to
`gitea.lumpiasty.xyz/lumpiasty/coredns-mikrotik`.
The Corefile is baked into the image — edit [`coredns/Corefile`](coredns/Corefile)
and push; the pipeline rebuilds and pushes a new image. Custom-built with a
minimal plugin set (~6-8 MB vs the official ~20 MB image) to fit the CRS flash.
See [docs/coredns.md](../docs/coredns.md) for design rationale, including why
the earlier NAT64/DNS64 approach was removed.
### 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.
## Deployment
The router configuration (container definitions, veth interfaces, bridge ports,
DNS settings, firewall) is managed declaratively via Ansible, not by manual CLI
commands. See [`ansible/roles/routeros/`](../ansible/roles/routeros/) and run:
```sh
cd ansible && ansible-playbook playbooks/routeros.yml
```
Containers do not auto-start on first image pull; after the initial deploy,
start manually once (subsequent boots are handled by `start-on-boot=yes`):
```
/container/start [find name=coredns]
```
+53
View File
@@ -0,0 +1,53 @@
# CoreDNS as a plain forwarding resolver with selective AAAA suppression.
#
# Background: the ISP provides no native IPv6, only a Hurricane Electric tunnel.
# HE addresses are flagged as datacenter ranges by some sites (endless CAPTCHAs,
# bot detection). To avoid this, IPv6 (AAAA) resolution is suppressed by default
# so clients use IPv4, while a whitelist keeps AAAA for domains where native
# IPv6 is wanted (our own services reachable over the HE prefix, and any domain
# explicitly trusted over IPv6).
#
# NAT64/DNS64 was tried and removed: it forced most traffic through a userspace
# Tayga translator, capping throughput at ~250 Mbps on the RB-class CPU (line
# rate is 1 Gbps) and adding two containers as a SPOF — all to avoid IPv6 egress
# we don't want in the first place. Plain AAAA suppression achieves the same
# IPv4-preferred outcome with zero datapath overhead.
#
# TODO: replace the global template suppression + whitelist with a plugin that
# suppresses AAAA only when the domain has no A record (so IPv6-only
# destinations still work). No such in-tree plugin exists yet.
# Whitelist: domains that keep AAAA resolution (native IPv6 via HE tunnel).
(aaaa_allowed) {
forward . 1.1.1.1 8.8.8.8 {
prefer_udp
}
cache 300
errors
log . {
class error
}
}
# Our own zone: services have native IPv6 on the HE prefix, keep AAAA.
lumpiasty.xyz:53 {
import aaaa_allowed
}
# Default: forward everything, but suppress AAAA so clients use IPv4 and
# avoid the HE tunnel's datacenter-flagged egress.
.:53 {
template IN AAAA {
rcode NOERROR
}
forward . 1.1.1.1 8.8.8.8 {
prefer_udp
}
cache 300
errors
log . {
class error
}
reload
health :8080
}
+32
View File
@@ -0,0 +1,32 @@
# Stage 1: build CoreDNS with minimal plugin set
FROM golang:1.26-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"]
+7
View File
@@ -0,0 +1,7 @@
errors:errors
log:log
health:health
template:template
cache:cache
forward:forward
reload:reload