Свой простой DynDNS сервер

от автора

Недавно ко мне обратились с вопросом «А какой внутренний IP адрес нужно указать в модеме для проброса порта на сервер?». Ответить на вопрос я не смог, так как давно не был на этом сервере, а квалификация человека на том конце не позволяла залогиниться на сервер и выполнить ip address show. Тогда я задумался над созданием своего простого аналога DynDNS сервера с возможностью хранения IP адресов всех интерфейсов клиента.

Серверная и клиентская часть реализованы на Python. ОС сервера — Debian 7, клиент — любой Linux с Python. Предполагается, что у вас уже есть свой домен и свои DNS сервера.

Серверная часть

Bind

Для начала сгенерируем необходимые ключи и настроим DNS-сервер:

dnssec-keygen -r /dev/urandom -a hmac-md5 -b 512 -n HOST dyndns.example.com. 

Из лобого из полученных файлов вида Kdyndns.example.com.+x+y.key или Kdyndns.example.com.+x+y.private запоминаем ключ зоны и добавляем зону (/etc/bind/named.conf.local):

key "dyndns.example.com." {     algorithm hmac-md5;     secret "вот тут запомненный ранее ключ"; };  zone "dyndns.example.com" {     type master;     file "/etc/bind/db.dyndns.example.com";     allow-query { any; };     allow-transfer { 127.0.0.1; };     allow-update { key dyndns.example.com.; }; }; 
Apache

Будем использовать Apache с mod-wsgi. Если у вас уже есть установленный и настроенный Apache, то просто устанавливаем один нужный пакет:

sudo aptitude install libapache2-mod-wsgi 

Включаем модуль wsgi:

sudo a2enmod wsgi sudo service apache2 reload 

Создаем новый VirtualHost с SSL:

/etc/apache2/sites-available/dyndns-ssl

<VirtualHost *:443> 	ServerName dyndns.example.com 	ServerAdmin admin@example.com 	DocumentRoot /var/www/tmp 	<Directory /> 		Options -FollowSymLinks 		AllowOverride None 		Order allow,deny 		Allow from all 	</Directory> 	<Directory /var/www/tmp> 		Options -Indexes -FollowSymLinks -MultiViews 		AllowOverride None 		deny from all 	</Directory> 	Alias /wsgi-scripts/ /var/www/dyndns/wsgi-scripts/ 	<Location /wsgi-scripts> 		SetHandler wsgi-script 		Options +ExecCGI 	</Location> 	SSLEngine on 	SSLCertificateFile /etc/ssl/localcerts/dyndns.example.com 	LogLevel info 	ErrorLog ${APACHE_LOG_DIR}/error_dyndns-ssl.example.com.log 	CustomLog ${APACHE_LOG_DIR}/access_dyndns-ssl.example.com.log combined </VirtualHost> 

И кладем главный скрипт update-dyndns.wsgi в /var/www/dyndns/wsgi-scripts/:

update-dyndns.wsgi

import dns.query import dns.tsigkeyring import dns.update import sys import datetime from IPy import IP from cgi import parse_qs, escape import hashlib   def application(environ, start_response): 	status = '200 OK' 	output = 'example.com DynDNS: ' 	ttl = 300 	domain = 'dyndns.example.com' 	salt = 'YourSalt'   	d = parse_qs(environ['QUERY_STRING'])   	hostname = escape(d.get('hostname',[''])[0]) 	main_address = escape(environ['REMOTE_ADDR']) 	interfacesRaw = [i.split('_') for i in [escape(interface) for interface in d.get('interface',[])]] 	checkRemote = escape(d.get('checkstring',[''])[0]) 	checkString = hashlib.md5(salt + hostname).hexdigest()   	interfaces=[] 	for interface in interfacesRaw: 		try: 			IP(interface[1]) 			interfaces.append(interface) 		except: 			output += 'Following addresses are not valid: ' + ' '.join(interface) 	timestampStr = "Last_update_at_" + str(datetime.datetime.now().strftime("%Y-%m-%d_%H:%M"))   	output += timestampStr + '; Hostname: ' + hostname + '; External address: ' + main_address + '; Check string: ' + checkRemote + '; Interfaces: ' + str(interfaces) 	if hostname != '' and main_address != '' and checkRemote == checkString: 		try: 			keyring = dns.tsigkeyring.from_text({domain+'.' : 'YourKeyring'}) 			update = dns.update.Update(domain, keyring=keyring) 			update.replace(hostname, ttl, 'a', main_address) 			update.replace(hostname, ttl, 'txt', timestampStr) 			if interfaces != []: 				for interface in interfaces: 					str1 = interface[0] + '.' + hostname + '.' + domain + '.' 					str2 = interface[0] + '.' + hostname + '.' + domain + '.' 					update.replace(str1, ttl, 'a', interface[1]) 					update.replace(str2, ttl, 'txt', timestampStr) 			nsResponse = dns.query.tcp(update, '127.0.0.1') 			output += '; update OK' 		except: 			output += '; Error inserting records!\n\n' 	else: 		print 'Error in query ' + escape(environ['QUERY_STRING']) 		output += '; Error in input' 	print output   	output = '' 	response_headers = [('Content-type', 'text/plain'),('Content-Length', str(len(output)))] 	start_response(status, response_headers) 	return [output] 

Общий принцип работы скрипта таков: клиент дергает на сервере определенный URL вида https://dyndns.example.com/wsgi-scripts/update-dyndns.wsgi?hostname=<hostname>&interface=<ifname>_<address>&checkstring=<checkstring>, где checkstring — некая строка, получаемая комбинацией соли (заданной в переменной salt), известной серверу и всем клиентам и имени хоста. interface может быть сколько угодно — они все попадут в DNS-зону. При получении правильного запроса от клиента, сервер добавляет или изменяет существующие A и TXT записи зоны вида interface.hostname.example.com и hostname.example.com.

Клиентская часть

Тут всё просто — по cron каждые, например, пол часа выполняем скрипт, предварительно задав переменные domain, myhostname и salt:

update_mydyndns.py

#!/usr/bin/python ################################################################## # #Configuration: domain='dyndns.example.com' myhostname='hostname-placeholder' ##################################################################  import socket import fcntl import struct import sys import array import hashlib import httplib import urllib  def all_interfaces(): 	is_64bits = sys.maxsize > 2**32 	struct_size = 40 if is_64bits else 32 	s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 	max_possible = 8 # initial value 	while True: 		bytes = max_possible * struct_size 		names = array.array('B', '\0' * bytes) 		outbytes = struct.unpack('iL', fcntl.ioctl(s.fileno(),0x8912,struct.pack('iL', bytes, names.buffer_info()[0])))[0] 		if outbytes == bytes: 			max_possible *= 2 		else: 			break 	namestr = names.tostring() 	lst = [] 	for i in range(0, outbytes, struct_size): 		name = namestr[i:i+16].split('\0', 1)[0] 		ip = socket.inet_ntoa(namestr[i+20:i+24]) 		if name != 'lo': 			lst.append((name,ip)) 	return lst  salt = 'YourSalt' checkString = hashlib.md5(salt + myhostname).hexdigest() requestData = {} requestData['hostname'] = myhostname requestData['checkstring'] = checkString requestData['interface'] = [j for j in [i[0]+'_'+i[1] for i in all_interfaces()]] requestString = urllib.urlencode(requestData,True)  conn = httplib.HTTPSConnection("dyndns.example.com", 443) conn.request("GET", "/wsgi-scripts/update-dyndns.wsgi?"+requestString) r1 = conn.getresponse() print r1.status, r1.reason 

Для удобства ещё скрипт получения всех записей зоны (положить на сервер в /var/www/dyndns/wsgi-scripts/):

get-whole-zone.wsgi

import dns.query import dns.zone   def application(environ, start_response): 	status = '200 OK' 	output = '' 	domain = 'dyndns.example.com'   	z = dns.zone.from_xfr(dns.query.xfr('127.0.0.1', 'dyndns.example.com')) 	names = z.nodes.keys() 	names.sort() 	for n in names: 		output += '\n' + z[n].to_text(n)   	response_headers = [('Content-type', 'text/plain'),('Content-Length', str(len(output)))] 	start_response(status, response_headers) 	return [output] 

P.S.

Я понимаю, что Python-код не претендует на красивость и в целом решение не очень секьюрно (был бы рад узнать, как можно обойтись без проверочной строки и соли), но у меня оно работает и решает кучу проблем. Надеюсь, кому-нибудь все это пригодится.

ссылка на оригинал статьи http://habrahabr.ru/post/188598/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *