Python – Demonologia
Python jest świetnym językiem do całej palety zastosowań. Służy on pomocą zarówno administratorom systemów, osobom które potrzebują napisać prosty parser, a nawet twórcom aplikacji webowych – dzięki takim frameworkom jak Django. W poniższym tekscie chciałbym pokazać jak łatwo jest stworzyć usługę systemową (ang. daemon) w języku Python, która będzie wykonywała dla nas określoną czynność – jako przykład przedstawię fakedns – bardzo prostego daemona DNS, który na dowolne zapytanie o adres IP będzie zwracał zawsze ten sam.
Dlaczego Python? Co z innymi językami?
Nie lepiej mieć aplikację którą można skompilować? Absolwenci kierunków informatycznych jeszcze kilka lat temu dowiadywali się na studiach, że najbardziej zaawansowanym językiem do wszelakich zastosowań jest Java. Mieli praktyczne doświadczenie z tworzenia aplikacji w Javie… i zaczęli pisać. Kod który chciałbym tu przedstawić pisałem dwa lata temu. Zanim się za niego zabrałem, zacząłem od szukania już gotowego rozwiązania za pomocą Google. Znalazłem, nawet kilka. Jedno w Javie. O zgrozo. Ja wiem, że się da. Wiem, że Java jest naprawde zaawansowanym językiem. Gotowa aplikacja wymaga jednak dużego nadkładu. Trzeba przygotować środowisko, przygotować konkretną ilość pamięci… i czekać kiedy aplikacja się wyłoży. Takie mam przynajmniej doświadczenia z aplikacjami w Javie, które pisali programiści zaraz po studiach, pracując w profesjonalnej firmie software’owej.
A inne języki? Natualnie. Jest C. Świetny język, ale czy mamy wystarczająco dużo czasu aby w nim pisać nawet małe aplikacje? .NET ? Wolne żarty. Nie dość, że pod Linuxem jest bardzo grubymi nićmi przyszywany, to i nie mamy pewności, czy Microsoft nie powie pewnego dnia hola, jedynie na ich, komercyjnej platformie. Naddatek potrzebnej mocy obliczeniowej, pamięci, wymaganej przestrzeni dyskowej… i ta świadomość, że nie jest to dopracowane i pewnie nigdy nie będzie – niczym Samba próbująca naśladować również kalekie i dziurawe, Microsoftowe domeny.
A co ze skryptowymi? Mnie na myśl przychodzą trzy alternatywy – Lua, Ruby i skrypt w bashu.
Lua – niesamowicie przejrzysty język. Piekielnie szybki, realizowany przez wręcz mikroskopijną bibliotekę. Na jego niekorzyść trzeba jednak zaliczyć wręcz niemal konieczność liczenia się z faktem, że jeśli rozwiązanie które potrzebujemy stworzyć, będzie wymagało biblioteki – będziemy zmuszeni długo jej szukać lub przygotować samemu jej obsługę za pomocą języka C.
Ruby – chyba najbliższy Pythonowi. Dlaczego nie. Bibliotek jest coraz więcej, jest wydajny. Miałem okazję jakiś czas temu nawet coś napisać w tym języku (a dokładnie – przepisać mały framework z Pythona), ale… jakoś w Pythonie pisze mi się lepiej. Co ważne – Ruby jest głęboko zakorzeniony w Perlu. Programistom mówi się jak powinni pisać kod, jednak zawsze mogą robić w to w stylu Perlowym – pisać szybko i wydajnie, jednak przeczytanie i przeanalizowanie go po jakimś czasie staje się piekłem. Kod w Pythonie jest de facto zawsze taki sam, nie zależnie kto by go pisał – czytanie go jest proste i przyjemne.
No i jeszcze uzupełnie o język skryptowy – Bash. Można w nim naprawdę dużo zrobić. Warto jednak zastanowić się na ile aplikacja będzie zaawansowana i czy zależy nam na wydajności. Podam przykład. Stworzyłem kiedyś skrypt w bashu generujący w oparciu o kilka plików wejściowych reguły dla firewalla, reguły shapera oraz treść dhcpd.conf. Skrypt wewnątrz wywoływał po kilka tysięcy razy awk/sed’a itp. Na maszynie jednoprocesorowej (Pentium 4 1.4 GHz), która pełniła funkcję routera dla sieci 100 Mbit/s, przy mniejszym obciążeniu – skrypt wykonywał się kilka minut. Przy większym – godzinę. Za każdym razem uruchamiając awk, spawnował odrębnego basha. Cóż. Przepisałem rozwiązanie na język Lua. Skrypt niezależnie od obciążenia maszyny wykonywał się w 0.02 sekundy. Jeśli byłby to Python czy Ruby – efekt byłby zapewne zbliżony do Lua.
Tworzenie usługi systemowej dla Linux/Unix
OK, zaczynamy. Usługa systemowa powinna przede wszystkim:
- wiedzieć jak pisać do sysloga
- zrzucić uprawnienia root’a
- odkleić się od stdin i stdout i przejść w tło
Jeśli taka usługa dodatkowo potrzebuje obsługiwać jakiś systemowy port TCP/UDP (o numerze niższym niż 1024) – zanim zrzuci ona prawa roota, powinna otworzyć sobie określonego socketa.
Nie będę tłumaczył poszczególnych fragmentu kodu ponieważ uważam, że jest on wystarczająco przejżysty.
fakedns.py
#!/usr/bin/python -u
# -*- coding: utf-8 -*-
" Fake DNS server: Always answer with the same, defined as conf.destip reply, listen at conf.listen "
class conf:
listen='ADRES_IP_NA_KTÓRYM_APLIKACJA_MA_NASŁUCHIWAĆ'
destip='ADRES_IP_NA_KTÓRY_APLIKACJA_BĘDZIE_KIEROWAĆ'
import os,sys,pwd,grp
import logging,logging.handlers
import socket
log=logging.getLogger('fakedns')
logging.basicConfig()
logging.root.setLevel(level=logging.INFO)
#logging.root.setLevel(level=logging.DEBUG)
hdlr=logging.handlers.SysLogHandler('/dev/log')
log.addHandler(hdlr)
def drop_privileges(uid_name='nobody',gid_name='nogroup'):
starting_uid=os.getuid()
starting_gid=os.getgid()
starting_uid_name=pwd.getpwuid(starting_uid)[0]
log.info('fakedns: drop_privileges: started as %s/%s'%(pwd.getpwuid(starting_uid)[0],grp.getgrgid(starting_gid)[0]))
if os.getuid()!=0:
log.info("fakedns: drop_privileges: already running as '%s'"%starting_uid_name)
return
if starting_uid==0:
# Get the uid/gid from the name
running_uid = pwd.getpwnam(uid_name)[2]
running_gid = grp.getgrnam(gid_name)[2]
# Try setting the new uid/gid
try:
os.setgid(running_gid)
except OSError,e:
log.error('Could not set effective group id: %s'%e)
try:
os.setuid(running_uid)
except OSError,e:
log.error('Could not set effective user id: %s'%e)
# Ensure a very convervative umask
new_umask=077
old_umask=os.umask(new_umask)
log.info('fakedns: drop_privileges: Old umask: %s, new umask: %s'%(oct(old_umask),oct(new_umask)))
final_uid=os.getuid()
final_gid=os.getgid()
log.info('fakedns: drop_privileges: running as %s/%s'%(pwd.getpwuid(final_uid)[0],grp.getgrgid(final_gid)[0]))
def become_daemon(home='.',stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
log.info('fakedns: deamonize')
# perform first fork
try:
if os.fork()>0:
sys.exit(0)
except OSError,e:
log.info("fakedns: 1st fork failed: %s"%e.strerror)
sys.exit(1)
os.setsid()
os.chdir(home)
os.umask(0)
# second fork
try:
if os.fork()>0:
sys.exit(0)
except OSError,e:
log.info("fakedns: 2nd fork failed: %s"%e.strerror)
sys.exit(1)
i=open(stdin,'r')
o=open(stdout,'a+')
e=open(stderr,'a+',0)
os.dup2(i.fileno(),sys.stdin.fileno())
os.dup2(o.fileno(),sys.stdout.fileno())
os.dup2(e.fileno(),sys.stderr.fileno())
class DNSQuery:
def __init__(self, data):
self.data=data
self.dominio=''
tipo = (ord(data[2]) >> 3) & 15 # Opcode bits
if tipo == 0: # Standard query
ini=12
lon=ord(data[ini])
while lon != 0:
self.dominio+=data[ini+1:ini+lon+1]+'.'
ini+=lon+1
lon=ord(data[ini])
def respuesta(self, ip):
packet=''
if self.dominio:
packet+=self.data[:2] + "\x81\x80"
# Questions and Answers Counts
packet+=self.data[4:6] + self.data[4:6] + '\x00\x00\x00\x00'
# Original Domain Name Question
packet+=self.data[12:]
# Pointer to domain name
packet+='\xc0\x0c'
# Response type, ttl and resource data length -> 4 bytes
packet+='\x00\x01\x00\x01\x00\x00\x00\x3c\x00\x04'
# 4bytes of IP
packet+=str.join('',map(lambda x: chr(int(x)), ip.split('.')))
return packet
if __name__ == '__main__':
udps = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
udps.bind((conf.listen,53))
except socket.error, (val,e):
log.info("fakedns: %s"%e)
sys.exit(1)
log.info('fakedns: dom.query. 60 IN A %s' % conf.destip)
drop_privileges()
become_daemon()
try:
while 1:
data, addr = udps.recvfrom(1024)
#log.info(addr)
p=DNSQuery(data)
udps.sendto(p.respuesta(conf.destip), addr)
#log.info('%s -> %s' % (p.dominio, conf.destip))
log.info('fakedns: %s -> %s'%(p.dominio,addr[0]))
except KeyboardInterrupt:
log.info('fakedns: done.' % (p.dominio, conf.destip))
udps.close()
except:
log.info('fakedns: crashed serving %s (%s)'%(addr, data))

