Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8197d615af | |||
|
d10c3efe68
|
|||
| ae7f58240c | |||
| 9f5d45d515 | |||
|
d3cb2a6e65
|
|||
|
679ebb3465
|
|||
| de10cba76c | |||
| b993115b41 | |||
| f02579a2f2 | |||
| e458949817 | |||
| 60ba0cfe90 | |||
| cb5f459182 | |||
|
d3a067886e
|
|||
|
33e01376b1
|
|||
|
374ee146fe
|
|||
|
2380cd16e4
|
|||
| 23ddd7c233 | |||
| a6bfb3d93c | |||
| 59f32659a1 | |||
| 199b14b810 | |||
| fc971e6e6c | |||
| aab4bc279c | |||
|
7f6439d64a
|
|||
| 5fed73515b | |||
| 1c092c8044 | |||
| 6bf31f0ae6 | |||
| 13a87e5b00 | |||
| 4e0a97d6f8 |
@@ -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]
|
||||
@@ -20,7 +20,7 @@ steps:
|
||||
- export VAULT_TOKEN=$(cat /woodpecker/.vault_id)
|
||||
- bao write -format json -f /kubernetes/creds/flux-reconcile > /woodpecker/kube_credentials
|
||||
- name: Construct Kubeconfig
|
||||
image: alpine/k8s:1.36.1
|
||||
image: alpine/k8s:1.36.2
|
||||
environment:
|
||||
KUBECONFIG: /woodpecker/kubeconfig
|
||||
commands:
|
||||
|
||||
@@ -21,7 +21,7 @@ steps:
|
||||
- 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
|
||||
- name: Run Renovate
|
||||
image: renovate/renovate:43.220.0
|
||||
image: renovate/renovate:43.227.1
|
||||
environment:
|
||||
RENOVATE_AUTODISCOVER: "true"
|
||||
RENOVATE_ENDPOINT: https://gitea.lumpiasty.xyz/api/v1
|
||||
|
||||
@@ -40,16 +40,18 @@
|
||||
- address: 2001:470:70:dd::2/64
|
||||
advertise: false
|
||||
interface: sit1
|
||||
- address: ::ffff:ffff:ffff:ffff/64
|
||||
from-pool: pool1
|
||||
# Static instead of from-pool: pool allocation is dynamic (first free /64,
|
||||
# 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
|
||||
- address: 2001:470:61a3:500:ffff:ffff:ffff:ffff/64
|
||||
interface: containers
|
||||
- address: 2001:470:61a3:100::1/64
|
||||
advertise: false
|
||||
interface: vlan4
|
||||
- address: ::ffff:ffff:ffff:ffff/64
|
||||
from-pool: pool1
|
||||
- address: 2001:470:61a3:a:ffff:ffff:ffff:ffff/64
|
||||
interface: vlan5
|
||||
- address: 2001:470:61a3:600::1/64
|
||||
advertise: false
|
||||
|
||||
@@ -65,6 +65,9 @@
|
||||
- bridge: containers
|
||||
interface: veth-tailscale
|
||||
comment: Tailscale container interface
|
||||
- bridge: containers
|
||||
interface: veth-coredns
|
||||
comment: CoreDNS container interface
|
||||
- bridge: bridge1
|
||||
interface: ether1
|
||||
pvid: 2
|
||||
@@ -152,24 +155,9 @@
|
||||
handle_absent_entries: remove
|
||||
handle_entries_content: remove_as_much_as_possible
|
||||
|
||||
- name: Configure DHCP networks
|
||||
community.routeros.api_modify:
|
||||
path: ip dhcp-server network
|
||||
data:
|
||||
- address: 192.168.0.0/24
|
||||
dns-server: 192.168.0.1
|
||||
gateway: 192.168.0.1
|
||||
- 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
|
||||
# Pool is no longer referenced — vlan2/vlan5 now use static addresses
|
||||
# (addressing.yml) so the RDNSS addresses in ND config are deterministic.
|
||||
# Kept defined for one run after migration; safe to delete afterwards.
|
||||
- name: Configure IPv6 pools
|
||||
community.routeros.api_modify:
|
||||
path: ipv6 pool
|
||||
@@ -188,7 +176,9 @@
|
||||
values:
|
||||
allow-remote-requests: true
|
||||
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
|
||||
community.routeros.api_modify:
|
||||
@@ -199,6 +189,12 @@
|
||||
forward-to: 100.100.100.100
|
||||
match-subdomain: true
|
||||
comment: Tailscale MagicDNS
|
||||
# Do NOT add a lumpiasty.xyz FWD entry here. RouterOS FWD entries return
|
||||
# NOERROR with an empty answer instead of relaying NXDOMAIN, which breaks
|
||||
# getaddrinfo search-domain processing (ENOTFOUND for valid names in k8s
|
||||
# pods). 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_entries_content: remove_as_much_as_possible
|
||||
|
||||
@@ -244,6 +240,22 @@
|
||||
handle_absent_entries: remove
|
||||
handle_entries_content: remove_as_much_as_possible
|
||||
|
||||
- name: Configure DHCP networks
|
||||
community.routeros.api_modify:
|
||||
path: ip dhcp-server network
|
||||
data:
|
||||
- address: 192.168.0.0/24
|
||||
dns-server: 192.168.0.1
|
||||
gateway: 192.168.0.1
|
||||
- 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
|
||||
community.routeros.api_find_and_modify:
|
||||
ignore_dynamic: false
|
||||
@@ -252,3 +264,21 @@
|
||||
default: true
|
||||
values:
|
||||
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:
|
||||
- dst: /var/lib/tailscale
|
||||
list: tailscale_state
|
||||
src: tailscale/state
|
||||
src: /tailscale/state
|
||||
handle_absent_entries: remove
|
||||
handle_entries_content: remove_as_much_as_possible
|
||||
|
||||
- name: Configure tailscale container
|
||||
- name: Configure containers
|
||||
community.routeros.api_modify:
|
||||
path: container
|
||||
data:
|
||||
- dns: 172.17.0.1
|
||||
- dns: 172.20.0.1
|
||||
interface: veth-tailscale
|
||||
logging: true
|
||||
mountlists: tailscale_state
|
||||
@@ -36,5 +36,12 @@
|
||||
remote-image: gitea.lumpiasty.xyz/lumpiasty/mikrotik-tailscale:stable
|
||||
root-dir: tailscale/root
|
||||
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_entries_content: remove_as_much_as_possible
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
keepalive-timeout: 2
|
||||
name: pppoe-gpon
|
||||
password: "{{ routeros_pppoe_password }}"
|
||||
use-peer-dns: true
|
||||
# Using CoreDNS container with DNS64
|
||||
use-peer-dns: false
|
||||
user: "{{ routeros_pppoe_username }}"
|
||||
handle_absent_entries: remove
|
||||
handle_entries_content: remove_as_much_as_possible
|
||||
@@ -37,5 +38,10 @@
|
||||
mac-address: 7E:7E:A1:B1:2A:7B
|
||||
name: veth-tailscale
|
||||
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_entries_content: remove_as_much_as_possible
|
||||
|
||||
@@ -18,7 +18,7 @@ spec:
|
||||
chart:
|
||||
spec:
|
||||
chart: authentik
|
||||
version: 2026.5.2
|
||||
version: 2026.5.3
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: authentik
|
||||
|
||||
@@ -7,7 +7,7 @@ metadata:
|
||||
name: gitea-shared-storage-lvmhdd
|
||||
namespace: openebs
|
||||
spec:
|
||||
capacity: 10Gi
|
||||
capacity: "21474836480"
|
||||
ownerNodeID: anapistula-delrosalae
|
||||
shared: "yes"
|
||||
thinProvision: "no"
|
||||
@@ -20,7 +20,7 @@ metadata:
|
||||
name: gitea-shared-storage-lvmhdd
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
storage: 20Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
@@ -41,6 +41,6 @@ spec:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
storage: 20Gi
|
||||
storageClassName: hdd-lvmpv
|
||||
volumeName: gitea-shared-storage-lvmhdd
|
||||
|
||||
+190
-13
@@ -14,6 +14,7 @@ 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_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_nothink_sampling: "--temp 1.0 --top-p 0.95 --top-k 64 -ctk q4_0 -ctv q4_0 --reasoning off"
|
||||
|
||||
hooks:
|
||||
on_startup:
|
||||
@@ -38,10 +39,24 @@ matrix:
|
||||
q4nt: "Qwen3.5-4B-GGUF-nothink:Q4_K_M"
|
||||
q4ht: "Qwen3.5-4B-heretic-GGUF:Q4_K_M"
|
||||
q4hnt: "Qwen3.5-4B-heretic-GGUF-nothink:Q4_K_M"
|
||||
g26xl: "gemma-4-26B-A4B-it:UD-Q4_K_XL"
|
||||
g26q2: "gemma-4-26B-A4B-it:UD-Q2_K_XL"
|
||||
ge4xl: "unsloth/gemma-4-E4B-it-GGUF:UD-Q4_K_XL"
|
||||
ge2xl: "unsloth/gemma-4-E2B-it-GGUF:UD-Q4_K_XL"
|
||||
g26xl: "gemma-4-26B-A4B-it-qat:UD-Q4_K_XL"
|
||||
g26xlnt: "gemma-4-26B-A4B-it-qat-nothink:UD-Q4_K_XL"
|
||||
g26mtp: "gemma-4-26B-A4B-it-qat-mtp: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"
|
||||
q36nt: "unsloth/Qwen3.6-35B-A3B-GGUF-nothink:UD-Q4_K_XL"
|
||||
haut: "HauhauCS/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive:Q4_K_M"
|
||||
@@ -54,7 +69,7 @@ matrix:
|
||||
|
||||
sets:
|
||||
# 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
|
||||
image_gen: "flux & stt"
|
||||
|
||||
@@ -152,38 +167,200 @@ models:
|
||||
${qwen35_nothink_args}
|
||||
${common_args}
|
||||
|
||||
"gemma-4-26B-A4B-it:UD-Q4_K_XL":
|
||||
"gemma-4-26B-A4B-it-qat:UD-Q4_K_XL":
|
||||
cmd: |
|
||||
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}
|
||||
${gemma4_sampling}
|
||||
${common_args}
|
||||
|
||||
"gemma-4-26B-A4B-it:UD-Q2_K_XL":
|
||||
"gemma-4-26B-A4B-it-qat-nothink:UD-Q4_K_XL":
|
||||
cmd: |
|
||||
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}
|
||||
${gemma4_sampling}
|
||||
${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: |
|
||||
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}
|
||||
${gemma4_sampling}
|
||||
${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: |
|
||||
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}
|
||||
${gemma4_sampling}
|
||||
${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":
|
||||
cmd: |
|
||||
llama-server
|
||||
|
||||
@@ -18,7 +18,7 @@ spec:
|
||||
spec:
|
||||
initContainers:
|
||||
- name: download-whisper
|
||||
image: gitea.lumpiasty.xyz/lumpiasty/llama-swap:unified-vulkan-parakeet-2026-06-09
|
||||
image: gitea.lumpiasty.xyz/lumpiasty/llama-swap:unified-vulkan-parakeet-2026-06-12
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
@@ -48,7 +48,7 @@ spec:
|
||||
mountPath: /root/.cache
|
||||
containers:
|
||||
- name: llama-swap
|
||||
image: gitea.lumpiasty.xyz/lumpiasty/llama-swap:unified-vulkan-parakeet-2026-06-09
|
||||
image: gitea.lumpiasty.xyz/lumpiasty/llama-swap:unified-vulkan-parakeet-2026-06-12
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- llama-swap
|
||||
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
# OpenAI-compatible Kokoro-FastAPI TTS server, CPU PyTorch backend.
|
||||
# Models baked into the image (no PVC needed).
|
||||
# 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:
|
||||
- containerPort: 8880
|
||||
name: http
|
||||
|
||||
@@ -15,7 +15,7 @@ spec:
|
||||
spec:
|
||||
initContainers:
|
||||
- name: prepare-home
|
||||
image: alpine:3.23.4
|
||||
image: alpine:3.24.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
|
||||
+110
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
- Containers on CRS<br>
|
||||
Access to every other network<br>
|
||||
IP: 172.17.0.1/16, 2001:470:61a3:500::/64<br>
|
||||
Static IP management
|
||||
IP: 172.20.0.1/24, 2001:470:61a3:500::/64<br>
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
- 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.
|
||||
|
||||
## Uplink
|
||||
|
||||
@@ -18,7 +18,7 @@ spec:
|
||||
chart:
|
||||
spec:
|
||||
chart: cert-manager-webhook-ovh
|
||||
version: 0.9.11
|
||||
version: 0.9.13
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cert-manager-webhook-ovh
|
||||
|
||||
@@ -23,7 +23,7 @@ spec:
|
||||
chart:
|
||||
spec:
|
||||
chart: cloudnative-pg
|
||||
version: 0.28.2
|
||||
version: 0.28.3
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: cnpg
|
||||
|
||||
@@ -23,7 +23,7 @@ spec:
|
||||
chart:
|
||||
spec:
|
||||
chart: openebs
|
||||
version: 4.4.0
|
||||
version: 4.5.0
|
||||
sourceRef:
|
||||
kind: HelmRepository
|
||||
name: openebs
|
||||
|
||||
@@ -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]
|
||||
```
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,7 @@
|
||||
errors:errors
|
||||
log:log
|
||||
health:health
|
||||
template:template
|
||||
cache:cache
|
||||
forward:forward
|
||||
reload:reload
|
||||
Reference in New Issue
Block a user