feat: add Proxmox DNS server implementation with dynamic hostname resolution

This commit is contained in:
2026-02-13 17:05:31 +01:00
parent cf5a6b2ed0
commit 67638dafcf
15 changed files with 1097 additions and 0 deletions
+87
View File
@@ -232,6 +232,7 @@ ansible-vault edit ansible/inventory/group_vars/all/vault.yml
## Key Infrastructure Components
- **Proxmox DNS** (LXC 118): Custom DNS server that resolves VM/LXC names via Proxmox API at `10.2.40.53`
- **Gitea** (LXC 114): Git server and CI/CD runner orchestrator at `gitea.renner.uno`
- **Gitea Runners** (LXC 905-906): Execute CI/CD pipelines
- **RustFS** (LXC 800): Self-hosted S3-compatible storage (MinIO) for OpenTofu state
@@ -240,6 +241,92 @@ ansible-vault edit ansible/inventory/group_vars/all/vault.yml
- **Immich** (LXC 225): Photo management service
- **Cloudflared** (LXC 117): Cloudflare tunnel endpoint
## Proxmox DNS Server
The repository includes a custom DNS server (`dns-server/`) that dynamically resolves VM and LXC hostnames by querying the Proxmox API.
### Architecture
- **Technology**: Python-based DNS server using `dnslib` and `proxmoxer`
- **Discovery**: Queries all Proxmox nodes for running VMs (via QEMU guest agent) and LXC containers
- **Caching**: Maintains an in-memory cache refreshed every 60 seconds
- **DHCP Support**: Works with dynamically assigned IPs by reading from Proxmox state
- **Static IP**: Deployed at `10.2.40.53` (LXC 118)
### How It Works
1. DNS query arrives for `hostname.renner.uno`
2. Server checks cache (60s TTL)
3. If cache miss, queries Proxmox API across all nodes
4. For VMs: Reads IP from QEMU guest agent network info
5. For LXCs: Reads IP from network config or interfaces
6. Returns A record with 300s TTL
### Deployment
The DNS server uses existing Proxmox credentials from the Ansible vault (no additional setup needed):
```bash
# Deploy via OpenTofu + Ansible
cd opentofu
tofu apply # Creates LXC 118 and runs ansible playbook
# Or deploy manually:
cd ansible
ansible-playbook -i inventory/ plays/install-proxmox-dns.yml
```
The role automatically uses these existing vault variables:
- `pve_api_url` - Proxmox API endpoint
- `pve_user` - Proxmox user (root@pam)
- `pve_token_id` - API token ID (ansible)
- `pve_token_secret` - API token secret (from vault)
### Integration with Network
To use the DNS server across your network:
1. **UniFi Configuration**:
- Settings > Networks > LAN
- DHCP Name Server: `10.2.40.53`
- This makes all DHCP clients use the Proxmox DNS server
2. **DNS Resolution Flow**:
- Query for `*.renner.uno` → Proxmox DNS Server (resolves from Proxmox API)
- UniFi DNS still handles CNAMEs (managed by OpenTofu)
- Upstream queries go to `10.2.40.1` (gateway)
### Monitoring and Troubleshooting
```bash
# Check service status
ssh root@10.2.40.53
systemctl status proxmox-dns
# View logs
journalctl -u proxmox-dns -f
# Test resolution locally
dig @localhost gitea.srv.renner.uno
# Test from another host
dig @10.2.40.53 gitea.srv.renner.uno
```
### Requirements for VMs
VMs must have QEMU guest agent installed and running for IP detection:
```bash
# Debian/Ubuntu
apt install qemu-guest-agent
systemctl enable --now qemu-guest-agent
# In Proxmox, ensure agent is enabled in VM options
```
LXC containers don't require any additional setup - IPs are read directly from Proxmox configuration.
## Special Considerations
### Running in CI
+3
View File
@@ -15,3 +15,6 @@ authentik_app
authentik-app-1 ansible_user=root
authentik-app-2 ansible_user=root
[proxmox_dns]
proxmox-dns.srv.renner.uno ansible_host=10.2.40.53 ansible_user=root
+28
View File
@@ -0,0 +1,28 @@
---
# Install Proxmox DNS Server
# This playbook installs the custom DNS server that resolves VM/LXC hostnames
# by querying the Proxmox API
- name: Install Proxmox DNS Server
hosts: proxmox_dns
become: yes
roles:
- proxmox-dns
post_tasks:
- name: Wait for DNS server to start
wait_for:
port: 53
timeout: 30
- name: Test DNS resolution
shell: |
dig @localhost gitea.srv.renner.uno +short
register: dns_test
failed_when: false
changed_when: false
- name: Display DNS test result
debug:
msg: "DNS test result: {{ dns_test.stdout }}"
+8
View File
@@ -0,0 +1,8 @@
---
# Proxmox DNS Server Secrets
# Encrypt this file with: ansible-vault encrypt proxmox_dns_vault.yml
# Proxmox API token for DNS server
# Create in Proxmox: Datacenter > Permissions > API Tokens
# Format: <token-id>!<token-name> = <secret>
proxmox_dns_api_token_value: "CHANGEME-your-proxmox-api-token-here"
@@ -0,0 +1,26 @@
---
# Proxmox DNS Server Role Defaults
proxmox_dns_install_dir: "/opt/proxmox-dns"
proxmox_dns_config_dir: "/etc/proxmox-dns"
# Proxmox API connection (uses existing vault variables)
proxmox_dns_api_host: "{{ pve_api_url | regex_replace('https://|http://|/api2/json', '') }}"
proxmox_dns_api_user: "{{ pve_user }}"
proxmox_dns_api_token_name: "{{ pve_token_id }}"
proxmox_dns_api_token_value: "{{ pve_token_secret }}"
# DNS server configuration
proxmox_dns_host: "0.0.0.0"
proxmox_dns_port: 53
proxmox_dns_suffix: ".renner.uno"
proxmox_dns_ttl: 300
proxmox_dns_cache_ttl: 60
proxmox_dns_upstream: "10.2.40.1"
# Python package requirements
proxmox_dns_python_packages:
- dnslib
- proxmoxer
- requests
- PyYAML
@@ -0,0 +1,11 @@
---
# Handlers for Proxmox DNS Server
- name: reload systemd
systemd:
daemon_reload: yes
- name: restart proxmox-dns
systemd:
name: proxmox-dns
state: restarted
+80
View File
@@ -0,0 +1,80 @@
---
# Proxmox DNS Server Installation
- name: Install system dependencies
apt:
name:
- python3
- python3-pip
- python3-venv
state: present
update_cache: yes
- name: Create installation directory
file:
path: "{{ proxmox_dns_install_dir }}"
state: directory
owner: root
group: root
mode: '0755'
- name: Create configuration directory
file:
path: "{{ proxmox_dns_config_dir }}"
state: directory
owner: root
group: root
mode: '0750'
- name: Copy DNS server application
copy:
src: "{{ playbook_dir }}/../dns-server/proxmox_dns.py"
dest: "{{ proxmox_dns_install_dir }}/proxmox_dns.py"
owner: root
group: root
mode: '0755'
- name: Copy requirements.txt
copy:
src: "{{ playbook_dir }}/../dns-server/requirements.txt"
dest: "{{ proxmox_dns_install_dir }}/requirements.txt"
owner: root
group: root
mode: '0644'
- name: Install Python dependencies
pip:
requirements: "{{ proxmox_dns_install_dir }}/requirements.txt"
state: present
executable: pip3
register: pip_install
retries: 3
delay: 5
until: pip_install is succeeded
- name: Create configuration file
template:
src: config.yaml.j2
dest: "{{ proxmox_dns_config_dir }}/config.yaml"
owner: root
group: root
mode: '0640'
notify: restart proxmox-dns
- name: Copy systemd service file
copy:
src: "{{ playbook_dir }}/../dns-server/proxmox-dns.service"
dest: /etc/systemd/system/proxmox-dns.service
owner: root
group: root
mode: '0644'
notify:
- reload systemd
- restart proxmox-dns
- name: Enable and start proxmox-dns service
systemd:
name: proxmox-dns
enabled: yes
state: started
daemon_reload: yes
@@ -0,0 +1,19 @@
# Proxmox DNS Server Configuration
# Managed by Ansible - Do not edit manually
proxmox:
host: "{{ proxmox_dns_api_host }}"
user: "{{ proxmox_dns_api_user }}"
token_name: "{{ proxmox_dns_api_token_name }}"
token_value: "{{ proxmox_dns_api_token_value }}"
verify_ssl: false
dns_host: "{{ proxmox_dns_host }}"
dns_port: {{ proxmox_dns_port }}
dns_suffix: "{{ proxmox_dns_suffix }}"
dns_ttl: {{ proxmox_dns_ttl }}
cache_ttl: {{ proxmox_dns_cache_ttl }}
upstream_dns: "{{ proxmox_dns_upstream }}"
+137
View File
@@ -0,0 +1,137 @@
# Proxmox DNS Server
A lightweight DNS server that dynamically resolves VM and LXC container hostnames by querying the Proxmox API.
## Features
- **Dynamic Resolution**: Queries Proxmox API in real-time to resolve hostnames
- **Automatic Discovery**: Discovers both VMs (with QEMU guest agent) and LXC containers
- **Caching**: Built-in caching to reduce API load
- **DHCP Compatible**: Works with DHCP-assigned IPs by reading from Proxmox state
- **Configurable**: YAML-based configuration
## How It Works
1. DNS query comes in for a hostname (e.g., `gitea.srv.renner.uno`)
2. Server strips the configured suffix and looks up `gitea`
3. Queries Proxmox API across all nodes for a VM/LXC named `gitea`
4. Returns the IP address from the VM's guest agent or LXC's network config
5. Caches the result for configured TTL
## Requirements
- Python 3.8+
- Access to Proxmox API (token or password)
- QEMU guest agent installed on VMs (for IP detection)
## Installation
### Manual Installation
```bash
# Install dependencies
pip3 install -r requirements.txt
# Create config directory
sudo mkdir -p /etc/proxmox-dns
# Copy and edit configuration
sudo cp config.yaml.example /etc/proxmox-dns/config.yaml
sudo nano /etc/proxmox-dns/config.yaml
# Run the server (requires root for port 53)
sudo python3 proxmox_dns.py -c /etc/proxmox-dns/config.yaml
```
### Systemd Service
```bash
# Copy service file
sudo cp proxmox-dns.service /etc/systemd/system/
# Reload systemd
sudo systemctl daemon-reload
# Enable and start
sudo systemctl enable proxmox-dns
sudo systemctl start proxmox-dns
# Check status
sudo systemctl status proxmox-dns
```
## Configuration
Edit `/etc/proxmox-dns/config.yaml`:
```yaml
proxmox:
host: "pve.renner.uno"
user: "root@pam"
token_name: "dns"
token_value: "your-token-here"
verify_ssl: false
dns_suffix: ".renner.uno"
dns_ttl: 300
cache_ttl: 60
```
### Creating a Proxmox API Token
1. Log into Proxmox web interface
2. Go to Datacenter > Permissions > API Tokens
3. Click "Add" to create a new token
4. User: `root@pam`, Token ID: `dns`
5. Uncheck "Privilege Separation" for full access
6. Copy the token secret to your config file
## Testing
```bash
# Test DNS resolution
dig @localhost gitea.srv.renner.uno
# Test with verbose logging
sudo python3 proxmox_dns.py -c /etc/proxmox-dns/config.yaml -v
```
## Integration with UniFi
To use this as your primary DNS server:
1. Deploy the DNS server on a static IP (e.g., `10.2.40.10`)
2. In UniFi Controller:
- Go to Settings > Networks > LAN
- Set DHCP Name Server to the DNS server IP
- Keep UniFi DNS for CNAME records (pointing to ingress, etc.)
## Troubleshooting
### VMs not resolving
- Ensure QEMU guest agent is installed and running in the VM
- Check that the VM is in "running" state
- Verify the guest agent can communicate with Proxmox
### LXCs not resolving
- For DHCP LXCs: The container must be running long enough for DHCP to assign an IP
- For static IPs: Check the container's network configuration in Proxmox
### Permission denied on port 53
- Run as root: `sudo python3 proxmox_dns.py`
- OR use a non-privileged port and forward: `sudo iptables -t nat -A PREROUTING -p udp --dport 53 -j REDIRECT --to-port 5353`
- OR grant capability: `sudo setcap 'cap_net_bind_service=+ep' /usr/bin/python3`
## Architecture
The DNS server uses a background thread that periodically queries the Proxmox API and caches results. When a DNS query arrives:
1. Check cache first (fast)
2. If cache miss or expired, trigger immediate API query
3. Update cache with fresh data
4. Return IP address or NXDOMAIN
This hybrid approach ensures low latency while keeping data fresh.
+223
View File
@@ -0,0 +1,223 @@
# Proxmox DNS Server - Setup Guide
This guide walks you through deploying the Proxmox DNS server in your homelab.
## Prerequisites
1. Proxmox cluster up and running
2. OpenTofu installed and configured
3. Ansible installed with vault password set up
4. Network access to Proxmox API
## Step 1: Verify Proxmox API Token
The DNS server uses the existing Proxmox API token already configured in the Ansible vault.
The following variables are used from the vault and group_vars:
- `pve_api_url`: Proxmox API URL (e.g., `https://jupiter.renner.uno:8006`)
- `pve_user`: Proxmox user (e.g., `root@pam`)
- `pve_token_id`: API token ID (e.g., `ansible`)
- `pve_token_secret`: API token secret (stored in vault)
These are already configured for your infrastructure, so no additional setup is needed!
## Step 2: Deploy with OpenTofu
```bash
cd opentofu
# Review what will be created
tofu plan
# Apply (creates LXC 118)
tofu apply
# The ansible_playbook parameter will automatically run the Ansible role
# to install and configure the DNS server
```
If you prefer to deploy the LXC manually or the automatic provisioning fails:
```bash
# Deploy LXC only (comment out ansible_playbook in lxc_118_proxmox_dns.tf)
tofu apply
# Then run Ansible manually
cd ../ansible
ansible-playbook -i inventory/ plays/install-proxmox-dns.yml
```
Note: The playbook automatically uses the vault password from `.vault_pass` via the `ANSIBLE_VAULT_PASSWORD_FILE` environment variable set in `.envrc`.
## Step 3: Verify Installation
```bash
# SSH to the DNS server
ssh root@10.2.40.53
# Check service status
systemctl status proxmox-dns
# View logs
journalctl -u proxmox-dns -f
# Test DNS resolution locally
dig @localhost gitea.srv.renner.uno
```
From another machine:
```bash
# Test from external host
dig @10.2.40.53 gitea.srv.renner.uno
# Should return the IP of your gitea VM/LXC
```
## Step 4: Configure UniFi to Use DNS Server
1. Log into UniFi Controller
2. Navigate to **Settings > Networks**
3. Edit your LAN network
4. Scroll to **DHCP**
5. Set **DHCP Name Server**: `Manual`
6. **DNS Server 1**: `10.2.40.53` (Proxmox DNS)
7. **DNS Server 2**: `10.2.40.1` or `8.8.8.8` (fallback)
8. Click **Apply Changes**
All DHCP clients will now use the Proxmox DNS server.
## Step 5: Configure VMs for Guest Agent (Optional but Recommended)
For VMs to be discoverable, they need the QEMU guest agent installed:
### Debian/Ubuntu VMs:
```bash
apt update
apt install qemu-guest-agent
systemctl enable --now qemu-guest-agent
```
### Rocky/AlmaLinux/RHEL VMs:
```bash
dnf install qemu-guest-agent
systemctl enable --now qemu-guest-agent
```
### In Proxmox UI:
1. Select the VM
2. Go to **Options**
3. **QEMU Guest Agent**: Enable
4. Reboot the VM
LXC containers don't need any special configuration - IPs are read directly from Proxmox.
## Testing
After setup, test that hostnames resolve:
```bash
# From any machine on the network
ping gitea.srv.renner.uno
ping authentik-db-1.renner.uno
# Using dig
dig @10.2.40.53 rustfs-1.srv.renner.uno
# Check DNS server logs for queries
ssh root@10.2.40.53 journalctl -u proxmox-dns -f
```
## Troubleshooting
### DNS server not starting
```bash
# Check logs for errors
journalctl -u proxmox-dns -n 100
# Common issues:
# - Invalid Proxmox token
# - Network connectivity to Proxmox API
# - Port 53 already in use
```
### VM not resolving
1. Check VM is running: Proxmox UI
2. Verify guest agent is installed and running: `systemctl status qemu-guest-agent`
3. Check Proxmox can see guest network:
```bash
qm guest cmd <vmid> network-get-interfaces
```
### LXC not resolving
1. Verify container is running
2. Check network configuration in Proxmox UI
3. For DHCP LXCs, ensure they have received an IP:
```bash
pct exec <ctid> -- ip addr show
```
### DNS queries timing out
1. Check firewall rules allow UDP/53
2. Verify DNS server is listening:
```bash
ssh root@10.2.40.53 netstat -ulnp | grep 53
```
3. Test locally first:
```bash
ssh root@10.2.40.53 dig @localhost test.renner.uno
```
## Maintenance
### Updating DNS Server
```bash
# Update code
cd ~/homelab
git pull
# Re-run Ansible
cd ansible
ansible-playbook -i inventory/ plays/install-proxmox-dns.yml --vault-password-file ../.vault_pass
# Or restart service if only config changed
ssh root@10.2.40.53 systemctl restart proxmox-dns
```
### Rotating API Token
1. Create new token in Proxmox UI
2. Update Ansible vault:
```bash
ansible-vault edit inventory/group_vars/all/vault.yml
# Update pve_token_secret value
```
3. Re-run Ansible to deploy new config
4. Delete old token in Proxmox UI
### Viewing Cache
The DNS server logs cache statistics on startup and refresh:
```bash
ssh root@10.2.40.53 journalctl -u proxmox-dns | grep "Fetched"
```
## Architecture Notes
- **Cache TTL**: 60 seconds (configurable in role defaults)
- **DNS Response TTL**: 300 seconds (5 minutes)
- **Query Pattern**: Background thread refreshes cache, DNS queries served from cache
- **Fallback**: Returns REFUSED for non-Proxmox queries (clients will try upstream DNS)
## Security Considerations
- API token has full read access to Proxmox (needed to query all nodes)
- DNS server runs as root (required for port 53)
- Config file contains API token (mode 0640, root:root)
- Systemd security hardening applied (ProtectSystem, PrivateTmp)
- No write operations to Proxmox - read-only
+31
View File
@@ -0,0 +1,31 @@
# Proxmox DNS Server Configuration
# Proxmox API connection
proxmox:
host: "pve.renner.uno"
user: "root@pam"
# Token-based authentication (recommended)
token_name: "dns"
token_value: "your-token-secret-here"
# OR password-based authentication
# password: "your-password-here"
verify_ssl: false
# DNS server settings
dns_host: "0.0.0.0" # Listen on all interfaces
dns_port: 53 # Standard DNS port (requires root or CAP_NET_BIND_SERVICE)
# DNS suffix for VM/LXC hostnames
dns_suffix: ".renner.uno"
# TTL for DNS responses (in seconds)
dns_ttl: 300
# Cache settings
cache_ttl: 60 # How often to refresh the cache (in seconds)
# Upstream DNS server for non-Proxmox queries
upstream_dns: "10.2.40.1"
+26
View File
@@ -0,0 +1,26 @@
[Unit]
Description=Proxmox DNS Server
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=root
Group=root
WorkingDirectory=/opt/proxmox-dns
ExecStart=/usr/bin/python3 /opt/proxmox-dns/proxmox_dns.py -c /etc/proxmox-dns/config.yaml
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log
AmbientCapabilities=CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
+367
View File
@@ -0,0 +1,367 @@
#!/usr/bin/env python3
"""
Proxmox DNS Server
A lightweight DNS server that dynamically resolves VM and LXC container hostnames
by querying the Proxmox API in real-time.
"""
import argparse
import logging
import signal
import sys
import time
from datetime import datetime, timedelta
from threading import Thread, Lock
from typing import Dict, Optional, Tuple
import yaml
from dnslib import DNSRecord, DNSHeader, RR, A, QTYPE, RCODE
from dnslib.server import DNSServer, BaseResolver
from proxmoxer import ProxmoxAPI
class ProxmoxResolver(BaseResolver):
"""DNS resolver that queries Proxmox API for VM/LXC hostnames"""
def __init__(self, config: dict):
self.config = config
self.cache: Dict[str, Tuple[str, datetime]] = {}
self.cache_lock = Lock()
self.cache_ttl = config.get('cache_ttl', 60)
self.proxmox_ttl = config.get('dns_ttl', 300)
self.suffix = config.get('dns_suffix', '.renner.uno')
# Proxmox connection
self.proxmox = None
self.last_connection_attempt = None
self.connection_retry_delay = 10
# Upstream DNS
self.upstream_dns = config.get('upstream_dns', '10.2.40.1')
# Start cache refresh thread
self.running = True
self.refresh_thread = Thread(target=self._cache_refresh_loop, daemon=True)
self.refresh_thread.start()
logging.info(f"ProxmoxResolver initialized with suffix: {self.suffix}")
logging.info(f"Upstream DNS: {self.upstream_dns}")
def _connect_proxmox(self) -> bool:
"""Establish connection to Proxmox API"""
now = datetime.now()
if self.last_connection_attempt and (now - self.last_connection_attempt).seconds < self.connection_retry_delay:
return False
self.last_connection_attempt = now
try:
auth_config = self.config['proxmox']
if 'token_name' in auth_config and 'token_value' in auth_config:
# Token-based auth
self.proxmox = ProxmoxAPI(
auth_config['host'],
user=auth_config['user'],
token_name=auth_config['token_name'],
token_value=auth_config['token_value'],
verify_ssl=auth_config.get('verify_ssl', False)
)
else:
# Password-based auth
self.proxmox = ProxmoxAPI(
auth_config['host'],
user=auth_config['user'],
password=auth_config['password'],
verify_ssl=auth_config.get('verify_ssl', False)
)
# Test connection
self.proxmox.version.get()
logging.info(f"Connected to Proxmox API at {auth_config['host']}")
return True
except Exception as e:
logging.error(f"Failed to connect to Proxmox API: {e}")
self.proxmox = None
return False
def _fetch_all_hosts(self) -> Dict[str, str]:
"""Fetch all VMs and LXCs from Proxmox API"""
if not self.proxmox:
if not self._connect_proxmox():
return {}
hosts = {}
try:
# Get all nodes
nodes = self.proxmox.nodes.get()
for node in nodes:
node_name = node['node']
# Get VMs (qemu)
try:
vms = self.proxmox.nodes(node_name).qemu.get()
for vm in vms:
if vm.get('status') != 'running':
continue
# Get VM config for agent network info
try:
config = self.proxmox.nodes(node_name).qemu(vm['vmid']).agent.get('network-get-interfaces')
# Find first non-loopback IPv4 address
for iface in config.get('result', []):
if iface.get('name') == 'lo':
continue
for ip_info in iface.get('ip-addresses', []):
if ip_info.get('ip-address-type') == 'ipv4':
ip = ip_info.get('ip-address')
if ip and not ip.startswith('127.'):
hostname = vm.get('name', '').lower()
if hostname:
hosts[hostname] = ip
logging.debug(f"VM: {hostname} -> {ip}")
break
if hostname in hosts:
break
except Exception as e:
# Guest agent might not be running or installed
logging.debug(f"Could not get agent info for VM {vm.get('name')}: {e}")
except Exception as e:
logging.error(f"Error fetching VMs from node {node_name}: {e}")
# Get LXC containers
try:
lxcs = self.proxmox.nodes(node_name).lxc.get()
for lxc in lxcs:
if lxc.get('status') != 'running':
continue
# Get LXC config for network info
try:
config = self.proxmox.nodes(node_name).lxc(lxc['vmid']).config.get()
# Parse network config (net0, net1, etc.)
for key, value in config.items():
if key.startswith('net') and isinstance(value, str):
# Parse format: "name=eth0,bridge=vmbr0,ip=10.2.40.123/24,..."
parts = dict(part.split('=', 1) for part in value.split(',') if '=' in part)
if 'ip' in parts:
ip = parts['ip'].split('/')[0]
if ip and ip != 'dhcp' and not ip.startswith('127.'):
hostname = lxc.get('name', '').lower()
if hostname:
hosts[hostname] = ip
logging.debug(f"LXC: {hostname} -> {ip}")
break
# If no static IP found, try to get from network interfaces (for DHCP)
if lxc.get('name', '').lower() not in hosts:
try:
interfaces = self.proxmox.nodes(node_name).lxc(lxc['vmid']).interfaces.get()
for iface in interfaces:
if iface.get('name') == 'lo':
continue
if 'inet' in iface:
ip = iface['inet'].split('/')[0]
if ip and not ip.startswith('127.'):
hostname = lxc.get('name', '').lower()
if hostname:
hosts[hostname] = ip
logging.debug(f"LXC (DHCP): {hostname} -> {ip}")
break
except:
# Interface endpoint might not be available in all Proxmox versions
pass
except Exception as e:
logging.debug(f"Could not get config for LXC {lxc.get('name')}: {e}")
except Exception as e:
logging.error(f"Error fetching LXCs from node {node_name}: {e}")
logging.info(f"Fetched {len(hosts)} hosts from Proxmox")
except Exception as e:
logging.error(f"Error fetching hosts from Proxmox: {e}")
self.proxmox = None
return hosts
def _cache_refresh_loop(self):
"""Background thread to refresh cache"""
while self.running:
try:
hosts = self._fetch_all_hosts()
with self.cache_lock:
# Update cache with fresh data
now = datetime.now()
for hostname, ip in hosts.items():
self.cache[hostname] = (ip, now)
# Clean up stale entries
stale_keys = [
key for key, (_, timestamp) in self.cache.items()
if (now - timestamp).seconds > self.cache_ttl * 2
]
for key in stale_keys:
del self.cache[key]
logging.debug(f"Removed stale cache entry: {key}")
except Exception as e:
logging.error(f"Error in cache refresh loop: {e}")
# Sleep for cache TTL
time.sleep(self.cache_ttl)
def _lookup_hostname(self, hostname: str) -> Optional[str]:
"""Look up hostname in cache, with fallback to live query"""
hostname = hostname.lower()
# Remove suffix if present
if hostname.endswith(self.suffix):
hostname = hostname[:-len(self.suffix)]
# Check cache first
with self.cache_lock:
if hostname in self.cache:
ip, timestamp = self.cache[hostname]
age = (datetime.now() - timestamp).seconds
if age < self.cache_ttl:
logging.debug(f"Cache hit: {hostname} -> {ip} (age: {age}s)")
return ip
# Cache miss or expired - trigger immediate refresh for this query
logging.debug(f"Cache miss: {hostname}, triggering refresh")
hosts = self._fetch_all_hosts()
if hostname in hosts:
ip = hosts[hostname]
with self.cache_lock:
self.cache[hostname] = (ip, datetime.now())
logging.info(f"Resolved: {hostname} -> {ip}")
return ip
logging.debug(f"Host not found: {hostname}")
return None
def resolve(self, request, handler):
"""Resolve DNS query"""
reply = request.reply()
qname = str(request.q.qname).rstrip('.')
qtype = QTYPE[request.q.qtype]
logging.debug(f"Query: {qname} ({qtype})")
# Only handle A records for our suffix
if qtype == 'A' and (qname.endswith(self.suffix.lstrip('.')) or '.' not in qname):
ip = self._lookup_hostname(qname)
if ip:
# Return A record
reply.add_answer(
RR(
rname=request.q.qname,
rtype=QTYPE.A,
rdata=A(ip),
ttl=self.proxmox_ttl
)
)
logging.info(f"Answered: {qname} -> {ip}")
return reply
else:
# NXDOMAIN - host not found
reply.header.rcode = RCODE.NXDOMAIN
logging.debug(f"NXDOMAIN: {qname}")
return reply
# For everything else, return REFUSED (let upstream handle it)
reply.header.rcode = RCODE.REFUSED
logging.debug(f"REFUSED: {qname} ({qtype})")
return reply
def stop(self):
"""Stop the resolver"""
self.running = False
if self.refresh_thread.is_alive():
self.refresh_thread.join(timeout=5)
def load_config(config_path: str) -> dict:
"""Load configuration from YAML file"""
with open(config_path, 'r') as f:
return yaml.safe_load(f)
def main():
parser = argparse.ArgumentParser(description='Proxmox DNS Server')
parser.add_argument('-c', '--config', default='/etc/proxmox-dns/config.yaml',
help='Configuration file path')
parser.add_argument('-v', '--verbose', action='store_true',
help='Enable verbose logging')
args = parser.parse_args()
# Setup logging
log_level = logging.DEBUG if args.verbose else logging.INFO
logging.basicConfig(
level=log_level,
format='%(asctime)s [%(levelname)s] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# Load configuration
try:
config = load_config(args.config)
except Exception as e:
logging.error(f"Failed to load configuration: {e}")
sys.exit(1)
# Create resolver
resolver = ProxmoxResolver(config)
# Start DNS server
dns_host = config.get('dns_host', '0.0.0.0')
dns_port = config.get('dns_port', 53)
try:
server = DNSServer(resolver, address=dns_host, port=dns_port)
server.start_thread()
logging.info(f"Proxmox DNS server started on {dns_host}:{dns_port}")
logging.info("Press Ctrl+C to stop")
# Setup signal handlers
def signal_handler(signum, frame):
logging.info("Shutting down...")
resolver.stop()
server.stop()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# Keep main thread alive
while True:
time.sleep(1)
except PermissionError:
logging.error(f"Permission denied to bind to port {dns_port}. Try running as root or use a port > 1024.")
sys.exit(1)
except Exception as e:
logging.error(f"Failed to start DNS server: {e}")
sys.exit(1)
if __name__ == '__main__':
main()
+4
View File
@@ -0,0 +1,4 @@
dnslib>=0.9.23
proxmoxer>=2.0.1
requests>=2.31.0
PyYAML>=6.0
+47
View File
@@ -0,0 +1,47 @@
# Proxmox DNS Server LXC
# Custom DNS server that resolves VM/LXC hostnames by querying Proxmox API
module "proxmox_dns" {
source = "./modules/proxmox_lxc"
hostname = "proxmox-dns.srv.renner.uno"
vmid = 118
target_node = "jupiter"
memory = 512
cores = 1
unprivileged = true
network_bridge = "v40srv"
ip_address = "10.2.40.53" # DNS-appropriate IP
storage = "local"
disk_size = "4G"
ostemplate = "local:vztmpl/debian-12-standard_12.12-1_amd64.tar.zst"
ssh_public_keys = local.common_ssh_keys
tags = "dns;infrastructure"
onboot = true
start = true
features = {
nesting = false
fuse = false
keyctl = false
mknod = false
}
# Optional: Automatically provision with Ansible after creation
ansible_playbook = "../ansible/plays/install-proxmox-dns.yml"
# No CNAMEs needed - this IS the DNS server
ingress_cnames = []
cnames = []
}
output "proxmox_dns_ip" {
value = "10.2.40.53"
description = "IP address of Proxmox DNS server"
}