Недавно ко мне обратились с вопросом «А какой внутренний IP адрес нужно указать в модеме для проброса порта на сервер?». Ответить на вопрос я не смог, так как давно не был на этом сервере, а квалификация человека на том конце не позволяла залогиниться на сервер и выполнить ip address show. Тогда я задумался над созданием своего простого аналога DynDNS сервера с возможностью хранения IP адресов всех интерфейсов клиента.
Серверная и клиентская часть реализованы на Python. ОС сервера — Debian 7, клиент — любой Linux с Python. Предполагается, что у вас уже есть свой домен и свои DNS сервера.
Для начала сгенерируем необходимые ключи и настроим DNS-сервер:
Из лобого из полученных файлов вида
Будем использовать Apache с mod-wsgi. Если у вас уже есть установленный и настроенный Apache, то просто устанавливаем один нужный пакет:
Включаем модуль wsgi:
Создаем новый VirtualHost с SSL:
И кладем главный скрипт
Общий принцип работы скрипта таков: клиент дергает на сервере определенный URL вида
Серверная и клиентская часть реализованы на 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 вида
dyndns.example.com/wsgi-scripts/update-dyndns.wsgi?hostname=&interface=_&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.wsgiimport 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-код не претендует на красивость и в целом решение не очень секьюрно (был бы рад узнать, как можно обойтись без проверочной строки и соли), но у меня оно работает и решает кучу проблем. Надеюсь, кому-нибудь все это пригодится.