Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2ae078754 |
@@ -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]
|
|
||||||
@@ -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.2
|
image: alpine/k8s:1.36.1
|
||||||
environment:
|
environment:
|
||||||
KUBECONFIG: /woodpecker/kubeconfig
|
KUBECONFIG: /woodpecker/kubeconfig
|
||||||
commands:
|
commands:
|
||||||
|
|||||||
@@ -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.227.1
|
image: renovate/renovate:43.217.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
|
||||||
|
|||||||
@@ -40,18 +40,16 @@
|
|||||||
- 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: containers
|
||||||
- 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
|
||||||
|
|||||||
@@ -65,9 +65,6 @@
|
|||||||
- 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
|
||||||
@@ -155,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
|
||||||
@@ -176,9 +188,7 @@
|
|||||||
values:
|
values:
|
||||||
allow-remote-requests: true
|
allow-remote-requests: true
|
||||||
cache-size: 20480
|
cache-size: 20480
|
||||||
# CoreDNS container: plain forwarder with selective AAAA suppression.
|
servers: 1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001
|
||||||
# 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:
|
||||||
@@ -189,12 +199,6 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -240,22 +244,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
|
||||||
|
|
||||||
- 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
|
||||||
@@ -264,21 +252,3 @@
|
|||||||
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
|
|
||||||
|
|||||||
@@ -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 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
|
interface: veth-tailscale
|
||||||
logging: true
|
logging: true
|
||||||
mountlists: tailscale_state
|
mountlists: tailscale_state
|
||||||
@@ -36,12 +36,5 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -38,10 +37,5 @@
|
|||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ metadata:
|
|||||||
name: gitea-shared-storage-lvmhdd
|
name: gitea-shared-storage-lvmhdd
|
||||||
namespace: openebs
|
namespace: openebs
|
||||||
spec:
|
spec:
|
||||||
capacity: "21474836480"
|
capacity: 10Gi
|
||||||
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: 20Gi
|
storage: 10Gi
|
||||||
accessModes:
|
accessModes:
|
||||||
- ReadWriteOnce
|
- ReadWriteOnce
|
||||||
persistentVolumeReclaimPolicy: Retain
|
persistentVolumeReclaimPolicy: Retain
|
||||||
@@ -41,6 +41,6 @@ spec:
|
|||||||
- ReadWriteOnce
|
- ReadWriteOnce
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
storage: 20Gi
|
storage: 10Gi
|
||||||
storageClassName: hdd-lvmpv
|
storageClassName: hdd-lvmpv
|
||||||
volumeName: gitea-shared-storage-lvmhdd
|
volumeName: gitea-shared-storage-lvmhdd
|
||||||
|
|||||||
+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-06-09
|
||||||
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-06-09
|
||||||
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
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ spec:
|
|||||||
value: "Users"
|
value: "Users"
|
||||||
- name: OAUTH_AUTO_REDIRECT
|
- name: OAUTH_AUTO_REDIRECT
|
||||||
value: "true"
|
value: "true"
|
||||||
# STT via parakeet-server (routed through llama-swap)
|
# STT via whisper-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 +92,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
|
||||||
|
|||||||
-110
@@ -1,110 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
# 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.
|
|
||||||
+2
-6
@@ -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.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 containers
|
Static IP management
|
||||||
|
|
||||||
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,10 +105,6 @@ 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.13
|
version: 0.9.11
|
||||||
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,43 +0,0 @@
|
|||||||
# 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]
|
|
||||||
```
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# 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
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# 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"]
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
errors:errors
|
|
||||||
log:log
|
|
||||||
health:health
|
|
||||||
template:template
|
|
||||||
cache:cache
|
|
||||||
forward:forward
|
|
||||||
reload:reload
|
|
||||||
Reference in New Issue
Block a user