Created
May 30, 2025 07:05
-
-
Save gregarendse/ba65577e38881efdff721c0b66343f28 to your computer and use it in GitHub Desktop.
DNSmasq to MikroTik DNS Static Converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
DNSmasq to MikroTik DNS Static Converter | |
Converts dnsmasq server configurations to MikroTik RouterOS DNS static entries. | |
Supports various dnsmasq directive formats and generates appropriate MikroTik commands. | |
""" | |
import re | |
import sys | |
import argparse | |
from pathlib import Path | |
from typing import List, Dict, Tuple, Optional | |
class DNSmasqConverter: | |
def __init__(self): | |
self.static_entries = [] | |
self.comments = [] | |
def parse_dnsmasq_line(self, line: str) -> Optional[Dict]: | |
"""Parse a single dnsmasq configuration line.""" | |
line = line.strip() | |
# Skip empty lines and comments | |
if not line or line.startswith('#'): | |
if line.startswith('#'): | |
return {'type': 'comment', 'content': line} | |
return None | |
# Handle server= directives (DNS forwarding) | |
# Examples: | |
# server=/example.com/192.168.1.10 | |
# server=/local/10.0.0.1 | |
# server=/company.internal/192.168.100.53#5353 | |
server_match = re.match(r'server=/(.*?)/(.*?)(?:#(\d+))?$', line) | |
if server_match: | |
domain = server_match.group(1) | |
dns_server = server_match.group(2) | |
port = server_match.group(3) | |
# Handle port specification | |
if port: | |
dns_server = f"{dns_server}#{port}" | |
return { | |
'type': 'server', | |
'domain': domain, | |
'dns_server': dns_server, | |
'original': line | |
} | |
# Handle address= directives (static DNS entries) | |
# Examples: | |
# address=/doubleclick.net/127.0.0.1 | |
# address=/ads.google.com/0.0.0.0 | |
address_match = re.match(r'address=/(.*?)/(.*?)$', line) | |
if address_match: | |
domain = address_match.group(1) | |
ip_address = address_match.group(2) | |
return { | |
'type': 'address', | |
'domain': domain, | |
'ip_address': ip_address, | |
'original': line | |
} | |
# Handle host-record= directives | |
# Example: host-record=router.local,192.168.1.1 | |
host_record_match = re.match(r'host-record=(.*?),(.*?)$', line) | |
if host_record_match: | |
hostname = host_record_match.group(1) | |
ip_address = host_record_match.group(2) | |
return { | |
'type': 'host_record', | |
'domain': hostname, | |
'ip_address': ip_address, | |
'original': line | |
} | |
# Handle cname= directives | |
# Example: cname=alias.example.com,target.example.com | |
cname_match = re.match(r'cname=(.*?),(.*?)$', line) | |
if cname_match: | |
alias = cname_match.group(1) | |
target = cname_match.group(2) | |
return { | |
'type': 'cname', | |
'alias': alias, | |
'target': target, | |
'original': line | |
} | |
# Return unparsed line for reference | |
return { | |
'type': 'unparsed', | |
'content': line, | |
'original': line | |
} | |
def convert_to_mikrotik(self, entry: Dict) -> str: | |
"""Convert a parsed dnsmasq entry to MikroTik format.""" | |
if entry['type'] == 'comment': | |
return f"# {entry['content'][1:].strip()}" | |
elif entry['type'] == 'server': | |
domain = entry['domain'] | |
dns_server = entry['dns_server'] | |
# Handle wildcard domains | |
if not domain.startswith('*.'): | |
domain = f"*.{domain}" | |
return f'/ip dns static add name={domain} type=FWD address={dns_server} comment="Forward {entry["domain"]} to {dns_server}"' | |
elif entry['type'] == 'address': | |
domain = entry['domain'] | |
ip_address = entry['ip_address'] | |
# Handle wildcard domains | |
if not domain.startswith('*.'): | |
domain = f"*.{domain}" | |
# Check if this is a blocking entry (0.0.0.0 or 127.0.0.1) | |
if ip_address in ['0.0.0.0', '127.0.0.1']: | |
return f'/ip dns static add name={domain} address={ip_address} comment="Block {entry["domain"]}"' | |
else: | |
return f'/ip dns static add name={domain} address={ip_address} comment="Static entry for {entry["domain"]}"' | |
elif entry['type'] == 'host_record': | |
hostname = entry['domain'] | |
ip_address = entry['ip_address'] | |
return f'/ip dns static add name={hostname} address={ip_address} comment="Host record for {hostname}"' | |
elif entry['type'] == 'cname': | |
alias = entry['alias'] | |
target = entry['target'] | |
return f'/ip dns static add name={alias} type=CNAME cname={target} comment="CNAME alias {alias} -> {target}"' | |
elif entry['type'] == 'unparsed': | |
return f"# UNPARSED: {entry['content']}" | |
return f"# UNKNOWN ENTRY TYPE: {entry}" | |
def process_file(self, input_file: str) -> List[str]: | |
"""Process a dnsmasq configuration file.""" | |
mikrotik_commands = [] | |
try: | |
with open(input_file, 'r', encoding='utf-8') as f: | |
lines = f.readlines() | |
except FileNotFoundError: | |
print(f"Error: File '{input_file}' not found.") | |
return [] | |
except Exception as e: | |
print(f"Error reading file: {e}") | |
return [] | |
mikrotik_commands.append("# Converted from dnsmasq configuration") | |
mikrotik_commands.append(f"# Source file: {input_file}") | |
mikrotik_commands.append("") | |
for line_num, line in enumerate(lines, 1): | |
try: | |
parsed = self.parse_dnsmasq_line(line) | |
if parsed: | |
mikrotik_cmd = self.convert_to_mikrotik(parsed) | |
mikrotik_commands.append(mikrotik_cmd) | |
except Exception as e: | |
mikrotik_commands.append(f"# ERROR processing line {line_num}: {line.strip()} - {e}") | |
return mikrotik_commands | |
def process_text(self, dnsmasq_text: str) -> List[str]: | |
"""Process dnsmasq configuration from text string.""" | |
lines = dnsmasq_text.strip().split('\n') | |
mikrotik_commands = [] | |
mikrotik_commands.append("# Converted from dnsmasq configuration") | |
mikrotik_commands.append("") | |
for line_num, line in enumerate(lines, 1): | |
try: | |
parsed = self.parse_dnsmasq_line(line) | |
if parsed: | |
mikrotik_cmd = self.convert_to_mikrotik(parsed) | |
mikrotik_commands.append(mikrotik_cmd) | |
except Exception as e: | |
mikrotik_commands.append(f"# ERROR processing line {line_num}: {line.strip()} - {e}") | |
return mikrotik_commands | |
def main(): | |
parser = argparse.ArgumentParser( | |
description='Convert dnsmasq server configuration to MikroTik DNS static entries', | |
formatter_class=argparse.RawDescriptionHelpFormatter, | |
epilog=""" | |
Examples: | |
%(prog)s -f /etc/dnsmasq.conf -o mikrotik_dns.rsc | |
%(prog)s -f dnsmasq.conf | |
%(prog)s -t "server=/local/192.168.1.1\\naddress=/ads.com/0.0.0.0" | |
Supported dnsmasq directives: | |
server=/domain/dns_server -> DNS forwarding | |
address=/domain/ip_address -> Static DNS entries | |
host-record=hostname,ip -> Host records | |
cname=alias,target -> CNAME records | |
""" | |
) | |
parser.add_argument('-f', '--file', help='Input dnsmasq configuration file') | |
parser.add_argument('-t', '--text', help='Dnsmasq configuration as text string') | |
parser.add_argument('-o', '--output', help='Output file for MikroTik commands') | |
parser.add_argument('--no-comments', action='store_true', help='Skip comment lines in output') | |
args = parser.parse_args() | |
converter = DNSmasqConverter() | |
if args.file: | |
commands = converter.process_file(args.file) | |
elif args.text: | |
commands = converter.process_text(args.text) | |
else: | |
# Interactive mode | |
print("Enter dnsmasq configuration (press Ctrl+D when done):") | |
try: | |
dnsmasq_text = sys.stdin.read() | |
commands = converter.process_text(dnsmasq_text) | |
except KeyboardInterrupt: | |
print("\nOperation cancelled.") | |
return | |
# Filter out comments if requested | |
if args.no_comments: | |
commands = [cmd for cmd in commands if not cmd.strip().startswith('#')] | |
# Output results | |
if args.output: | |
try: | |
with open(args.output, 'w', encoding='utf-8') as f: | |
for cmd in commands: | |
f.write(cmd + '\n') | |
print(f"MikroTik commands written to: {args.output}") | |
except Exception as e: | |
print(f"Error writing output file: {e}") | |
return | |
else: | |
for cmd in commands: | |
print(cmd) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment