Autarch Will Control The Internet
This commit is contained in:
324
core/dns_service.py
Normal file
324
core/dns_service.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""AUTARCH DNS Service Manager — controls the Go-based autarch-dns binary."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
from core.paths import find_tool, get_data_dir
|
||||
except ImportError:
|
||||
def find_tool(name):
|
||||
import shutil
|
||||
return shutil.which(name)
|
||||
def get_data_dir():
|
||||
return str(Path(__file__).parent.parent / 'data')
|
||||
|
||||
try:
|
||||
import requests
|
||||
_HAS_REQUESTS = True
|
||||
except ImportError:
|
||||
_HAS_REQUESTS = False
|
||||
|
||||
|
||||
class DNSServiceManager:
|
||||
"""Manage the autarch-dns Go binary (start/stop/API calls)."""
|
||||
|
||||
def __init__(self):
|
||||
self._process = None
|
||||
self._pid = None
|
||||
self._config = None
|
||||
self._config_path = os.path.join(get_data_dir(), 'dns', 'config.json')
|
||||
self._load_config()
|
||||
|
||||
def _load_config(self):
|
||||
if os.path.exists(self._config_path):
|
||||
try:
|
||||
with open(self._config_path, 'r') as f:
|
||||
self._config = json.load(f)
|
||||
except Exception:
|
||||
self._config = None
|
||||
if not self._config:
|
||||
self._config = {
|
||||
'listen_dns': '0.0.0.0:53',
|
||||
'listen_api': '127.0.0.1:5380',
|
||||
'api_token': os.urandom(16).hex(),
|
||||
'upstream': [], # Empty = pure recursive from root hints
|
||||
'cache_ttl': 300,
|
||||
'zones_dir': os.path.join(get_data_dir(), 'dns', 'zones'),
|
||||
'dnssec_keys_dir': os.path.join(get_data_dir(), 'dns', 'keys'),
|
||||
'log_queries': True,
|
||||
}
|
||||
self._save_config()
|
||||
|
||||
def _save_config(self):
|
||||
os.makedirs(os.path.dirname(self._config_path), exist_ok=True)
|
||||
with open(self._config_path, 'w') as f:
|
||||
json.dump(self._config, f, indent=2)
|
||||
|
||||
@property
|
||||
def api_base(self) -> str:
|
||||
addr = self._config.get('listen_api', '127.0.0.1:5380')
|
||||
return f'http://{addr}'
|
||||
|
||||
@property
|
||||
def api_token(self) -> str:
|
||||
return self._config.get('api_token', '')
|
||||
|
||||
def find_binary(self) -> str:
|
||||
"""Find the autarch-dns binary."""
|
||||
binary = find_tool('autarch-dns')
|
||||
if binary:
|
||||
return binary
|
||||
# Check common locations
|
||||
base = Path(__file__).parent.parent
|
||||
candidates = [
|
||||
base / 'services' / 'dns-server' / 'autarch-dns',
|
||||
base / 'services' / 'dns-server' / 'autarch-dns.exe',
|
||||
base / 'tools' / 'windows-x86_64' / 'autarch-dns.exe',
|
||||
base / 'tools' / 'linux-arm64' / 'autarch-dns',
|
||||
base / 'tools' / 'linux-x86_64' / 'autarch-dns',
|
||||
]
|
||||
for c in candidates:
|
||||
if c.exists():
|
||||
return str(c)
|
||||
return None
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Check if the DNS service is running."""
|
||||
# Check process
|
||||
if self._process and self._process.poll() is None:
|
||||
return True
|
||||
# Check by API
|
||||
try:
|
||||
resp = self._api_get('/api/status')
|
||||
return resp.get('ok', False)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def start(self) -> dict:
|
||||
"""Start the DNS service."""
|
||||
if self.is_running():
|
||||
return {'ok': True, 'message': 'DNS service already running'}
|
||||
|
||||
binary = self.find_binary()
|
||||
if not binary:
|
||||
return {'ok': False, 'error': 'autarch-dns binary not found. Build it with: cd services/dns-server && go build'}
|
||||
|
||||
# Ensure zone dirs exist
|
||||
os.makedirs(self._config.get('zones_dir', ''), exist_ok=True)
|
||||
os.makedirs(self._config.get('dnssec_keys_dir', ''), exist_ok=True)
|
||||
|
||||
# Save config for the Go binary to read
|
||||
self._save_config()
|
||||
|
||||
cmd = [
|
||||
binary,
|
||||
'-config', self._config_path,
|
||||
]
|
||||
|
||||
try:
|
||||
kwargs = {
|
||||
'stdout': subprocess.DEVNULL,
|
||||
'stderr': subprocess.DEVNULL,
|
||||
}
|
||||
if sys.platform == 'win32':
|
||||
kwargs['creationflags'] = (
|
||||
subprocess.CREATE_NEW_PROCESS_GROUP |
|
||||
subprocess.CREATE_NO_WINDOW
|
||||
)
|
||||
else:
|
||||
kwargs['start_new_session'] = True
|
||||
|
||||
self._process = subprocess.Popen(cmd, **kwargs)
|
||||
self._pid = self._process.pid
|
||||
|
||||
# Wait for API to be ready
|
||||
for _ in range(30):
|
||||
time.sleep(0.5)
|
||||
try:
|
||||
resp = self._api_get('/api/status')
|
||||
if resp.get('ok'):
|
||||
return {
|
||||
'ok': True,
|
||||
'message': f'DNS service started (PID {self._pid})',
|
||||
'pid': self._pid,
|
||||
}
|
||||
except Exception:
|
||||
if self._process.poll() is not None:
|
||||
return {'ok': False, 'error': 'DNS service exited immediately — may need admin/root for port 53'}
|
||||
continue
|
||||
|
||||
return {'ok': False, 'error': 'DNS service started but API not responding'}
|
||||
except PermissionError:
|
||||
return {'ok': False, 'error': 'Permission denied — DNS on port 53 requires admin/root'}
|
||||
except Exception as e:
|
||||
return {'ok': False, 'error': str(e)}
|
||||
|
||||
def stop(self) -> dict:
|
||||
"""Stop the DNS service."""
|
||||
if self._process and self._process.poll() is None:
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
self._process.terminate()
|
||||
else:
|
||||
os.kill(self._process.pid, signal.SIGTERM)
|
||||
self._process.wait(timeout=5)
|
||||
except Exception:
|
||||
self._process.kill()
|
||||
self._process = None
|
||||
self._pid = None
|
||||
return {'ok': True, 'message': 'DNS service stopped'}
|
||||
return {'ok': True, 'message': 'DNS service was not running'}
|
||||
|
||||
def status(self) -> dict:
|
||||
"""Get service status."""
|
||||
running = self.is_running()
|
||||
result = {
|
||||
'running': running,
|
||||
'pid': self._pid,
|
||||
'listen_dns': self._config.get('listen_dns', ''),
|
||||
'listen_api': self._config.get('listen_api', ''),
|
||||
}
|
||||
if running:
|
||||
try:
|
||||
resp = self._api_get('/api/status')
|
||||
result.update(resp)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
# ── API wrappers ─────────────────────────────────────────────────────
|
||||
|
||||
def _api_get(self, endpoint: str) -> dict:
|
||||
if not _HAS_REQUESTS:
|
||||
return self._api_urllib(endpoint, 'GET')
|
||||
resp = requests.get(
|
||||
f'{self.api_base}{endpoint}',
|
||||
headers={'Authorization': f'Bearer {self.api_token}'},
|
||||
timeout=5,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
def _api_post(self, endpoint: str, data: dict = None) -> dict:
|
||||
if not _HAS_REQUESTS:
|
||||
return self._api_urllib(endpoint, 'POST', data)
|
||||
resp = requests.post(
|
||||
f'{self.api_base}{endpoint}',
|
||||
headers={'Authorization': f'Bearer {self.api_token}', 'Content-Type': 'application/json'},
|
||||
json=data or {},
|
||||
timeout=5,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
def _api_delete(self, endpoint: str) -> dict:
|
||||
if not _HAS_REQUESTS:
|
||||
return self._api_urllib(endpoint, 'DELETE')
|
||||
resp = requests.delete(
|
||||
f'{self.api_base}{endpoint}',
|
||||
headers={'Authorization': f'Bearer {self.api_token}'},
|
||||
timeout=5,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
def _api_put(self, endpoint: str, data: dict = None) -> dict:
|
||||
if not _HAS_REQUESTS:
|
||||
return self._api_urllib(endpoint, 'PUT', data)
|
||||
resp = requests.put(
|
||||
f'{self.api_base}{endpoint}',
|
||||
headers={'Authorization': f'Bearer {self.api_token}', 'Content-Type': 'application/json'},
|
||||
json=data or {},
|
||||
timeout=5,
|
||||
)
|
||||
return resp.json()
|
||||
|
||||
def _api_urllib(self, endpoint: str, method: str, data: dict = None) -> dict:
|
||||
"""Fallback using urllib (no requests dependency)."""
|
||||
import urllib.request
|
||||
url = f'{self.api_base}{endpoint}'
|
||||
body = json.dumps(data).encode() if data else None
|
||||
req = urllib.request.Request(
|
||||
url, data=body, method=method,
|
||||
headers={
|
||||
'Authorization': f'Bearer {self.api_token}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
return json.loads(resp.read())
|
||||
|
||||
# ── High-level zone operations ───────────────────────────────────────
|
||||
|
||||
def list_zones(self) -> list:
|
||||
return self._api_get('/api/zones').get('zones', [])
|
||||
|
||||
def create_zone(self, domain: str) -> dict:
|
||||
return self._api_post('/api/zones', {'domain': domain})
|
||||
|
||||
def get_zone(self, domain: str) -> dict:
|
||||
return self._api_get(f'/api/zones/{domain}')
|
||||
|
||||
def delete_zone(self, domain: str) -> dict:
|
||||
return self._api_delete(f'/api/zones/{domain}')
|
||||
|
||||
def list_records(self, domain: str) -> list:
|
||||
return self._api_get(f'/api/zones/{domain}/records').get('records', [])
|
||||
|
||||
def add_record(self, domain: str, rtype: str, name: str, value: str,
|
||||
ttl: int = 300, priority: int = 0) -> dict:
|
||||
return self._api_post(f'/api/zones/{domain}/records', {
|
||||
'type': rtype, 'name': name, 'value': value,
|
||||
'ttl': ttl, 'priority': priority,
|
||||
})
|
||||
|
||||
def delete_record(self, domain: str, record_id: str) -> dict:
|
||||
return self._api_delete(f'/api/zones/{domain}/records/{record_id}')
|
||||
|
||||
def setup_mail_records(self, domain: str, mx_host: str = '',
|
||||
dkim_key: str = '', spf_allow: str = '') -> dict:
|
||||
return self._api_post(f'/api/zones/{domain}/mail-setup', {
|
||||
'mx_host': mx_host, 'dkim_key': dkim_key, 'spf_allow': spf_allow,
|
||||
})
|
||||
|
||||
def enable_dnssec(self, domain: str) -> dict:
|
||||
return self._api_post(f'/api/zones/{domain}/dnssec/enable')
|
||||
|
||||
def disable_dnssec(self, domain: str) -> dict:
|
||||
return self._api_post(f'/api/zones/{domain}/dnssec/disable')
|
||||
|
||||
def get_metrics(self) -> dict:
|
||||
return self._api_get('/api/metrics').get('metrics', {})
|
||||
|
||||
def get_config(self) -> dict:
|
||||
return self._config.copy()
|
||||
|
||||
def update_config(self, updates: dict) -> dict:
|
||||
for k, v in updates.items():
|
||||
if k in self._config:
|
||||
self._config[k] = v
|
||||
self._save_config()
|
||||
# Also update running service
|
||||
try:
|
||||
return self._api_put('/api/config', updates)
|
||||
except Exception:
|
||||
return {'ok': True, 'message': 'Config saved (service not running)'}
|
||||
|
||||
|
||||
# ── Singleton ────────────────────────────────────────────────────────────────
|
||||
|
||||
_instance = None
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def get_dns_service() -> DNSServiceManager:
|
||||
global _instance
|
||||
if _instance is None:
|
||||
with _lock:
|
||||
if _instance is None:
|
||||
_instance = DNSServiceManager()
|
||||
return _instance
|
||||
Reference in New Issue
Block a user