Mega Sekurak Hacking Party w Krakowie! 26-27.10.2026 r.
Tworzenie narzędzi sieciowych w Pythonie z użyciem socketów
Wstęp
W poprzednim wpisie poświęconym tworzeniu narzędzi w Pythonie przedstawiłem kilka prostych skryptów korzystających z modułów sys, os oraz httplib. W tym wpisie „zejdziemy” nieco niżej i zobaczymy, jak tworzyć skrypty, które komunikują się z siecią przez sockety.
Czym są sockety (gniazda)?
Jest to pewien mechanizm umożliwiający otwarcie kanału komunikacji pomiędzy hostami. Każde gniazdo posiada adres IP oraz numer portu i może przesyłać trzy rodzaje pakietów:
- datagramy (pakiety UDP) — są to tzw. „datagram sockets” — ten kanał komunikacji wykorzystuje protokół UDP, czyli połączenie jest bezstanowe. Gniazdko wysyła pakiet UDP do docelowego hosta/portu i nie sprawdza, czy komunikacja zakończyła się powodzeniem. Przykładem usługi działającej z użyciem UDP jest DNS (Domain Name Server, port 53);
- strumienie (pakiety TCP) — czyli „stream sockets” — te gniazdka wysyłają dane za pomocą protokołu TCP, a zatem otwierają kanał komunikacji na określonym porcie, dane wysyłane są w pakietach TCP, gniazdko (socket) nie zamyka połączenia i oczekuje na odpowiedź od hosta docelowego. Przykładem usługi korzystającej z TCP jest HTTP (HyperText Transfer Protocol, port 80 — gdy przeglądarka internetowa chce pobrać zasób z serwera WWW, robi to właśnie poprzez zestawienie kanału komunikacji TCP na porcie 80);
- raw sockets (IP sockets) — ostatni rodzaj to gniazdka, które są w stanie wysyłać pakiety IP z pominięciem obu przedstawionych powyżej rodzajów pakietów, które są niejako opakowaniem dla danych (UDP i TCP). Raw IP sockets pozwalają na zdefiniowanie pakietu IP z dowolnym nagłówkiem i przesłanie go dowolnie zdefiniowanym protokołem (np. ICMP).
Pierwszy skrypt
Przejdźmy zatem do praktyki. Na początek zobaczmy, co potrafi moduł socket — napiszemy prosty odpowiednik polecenia host, które w przypadku podania nazwy domenowej serwera zwraca nam jego adres IP.
#!/usr/bin/python
#
# uzycie:
# ./host.py domena
#
import socket, sys # [1]
def getHostIP(domain_name):
ip_addr = socket.gethostbyname(domain_name) # [2]
return ip_addr
if __name__ == '__main__':
domain = sys.argv[1]
print "Adres IP dla domeny %s to %s" % (domain, getHostIP(domain))
Po zapisaniu i uruchomieniu skryptu, jako argument podajmy mu domenę serwisu Sekurak, czyli sekurak.pl. Powinniśmy otrzymać rezultat analogiczny do poniższego:
$ ./host.py sekurak.pl Adres IP dla domeny sekurak.pl to 178.32.219.59
Po zaimportowaniu modułu socket [1], w funkcji getHostIP() użyliśmy jednej z jego metod, [2], by uzyskać adres IP nazwy domenowej.
Oczywiście to tylko wycinek możliwości modułu socket. Warto przyjrzeć się dostępnej dokumentacji.
Architektura klient-serwer
Pora na coś bardziej złożonego. Napiszmy prostą implementację klienta, który będzie wysyłał dane przez protokół TCP, oraz serwera, który te dane odbierze. Zacznijmy od strony klienckiej:
#!/usr/bin/python
#
# KLIENT
#
import socket
def sendPacket():
proto = socket.getprotobyname('tcp') # [1]
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto) # [2]
try:
s.connect(("127.0.0.1", 2222)) # [3]
s.send("Hello world") # [4]
resp = s.recv(1024) # [5]
print resp
except socket.error:
pass
finally:
s.close()
if __name__ == '__main__':
sendPacket()
Pierwszą rzeczą, jaką musimy zrobić, zanim zdefiniujemy gniazdo, jest wybór protokołu, w jakim będzie pracować. W tym przykładzie będzie to TCP [1]. Następnie tworzymy gniazdko [2], podając niezbędne parametry: socket.AF_INET określa, że korzystamy z adresacji IPv4, socket.SOCK_STREAM to gniazdo przesyłające dane strumieniowo, ostatnim argumentem jest wybrany wcześniej protokół.
W [3] nawiązujemy połączenie z serwerem, który będzie nasłuchiwał połączeń na porcie 2222. Roboczo możemy zastosować np. netcata i nakazać mu nasłuchiwanie na docelowym porcie:
$ nc -l 2222
Po uruchomieniu przykładu powinniśmy uzyskać rezultat podobny do zamieszczonego poniżej:

Screen 1. Górna konsola to uruchomiony skrypt Pythona z listingu powyżej; w dolnej – nasłuchujący na porcie 2222 netcat i odebrana wiadomość.
Spróbujmy teraz zastąpić netcata skryptem napisanym w Pythonie. W tym celu musimy stworzyć prosty serwer, który będzie w stanie odebrać i wyświetlić dane wysłane klientem z poprzedniego przykładu.
#!/usr/bin/python
#
# SERWER
#
import socket
def server():
proto = socket.getprotobyname('tcp') # [1]
serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM, proto)
serv.bind(("localhost", 2222)) # [2]
serv.listen(1) # [3]
return serv
serv = server()
while 1:
conn, addr = serv.accept() # [4]
while 1:
message = conn.recv(64) # [5]
if message:
conn.send('Hi, I am a server, I received: ' + message)
else:
break
conn.close()
Podobnie jak w poprzednim przykładzie, tworzymy gniazdo TCP [1], ale tym razem, zamiast inicjować połączenie, otwieramy port 2222 [2], do którego „podpinamy” nasze gniazdo, od tego momentu nasłuchując danych, które będą zaadresowane na ten port [3]. Argument metody listen(1) to ilość równoczesnych połączeń, jaką serwer ma obsłużyć — dla naszego przykładu wystarczy jedno.
Po uruchomieniu serwera i nawiązaniu połączenia (które tworzy identyfikator conn — po tym identyfikatorze będziemy mogli odczytać dane, które nim nadeszły) odczytujemy [4] ze strumienia po 64 bajty [5] tak długo, jak pojawiają się nowe dane (w naszym przykładzie jest to tylko ciąg „Hello World!!!”, więc pętla zakończy się po pierwszej iteracji). Odebrane dane odsyłamy z powrotem, korzystając z identyfikatora połączenia conn — ponieważ klient ma zaimplementowane odbieranie danych, rezultatem będzie wyświetlenie zwróconego przez serwer ciągu w konsoli klienta (listing KLIENT, linijka [5]).

Screen 2. Górna konsola to nasz serwer, w dolnej – ponownie uruchomiony klient oraz odpowiedź serwera.
Prosta implementacja polecenia traceroute
Pora na bardziej użyteczny przykład. Poniżej znajduje się kod źródłowy implementujący funkcjonalność polecenia traceroute (traceroute(8) – Linux man page, jego odpowiednikiem w systemach z rodziny Windows jest tracert). Oczywiście jest to bardzo uproszczony odpowiednik, ale doskonale nadaje się na przedstawienie zagadnienia definiowania gniazd sieciowych.
Przykład pochodzi ze strony Learning by doing: Writing your own traceroute in 8 easy steps.
Na początek kod źródłowy:
#!/usr/bin/python
import socket,sys
def main(dest_name):
dest_addr = socket.gethostbyname(dest_name) # [1]
port = 33434
max_hops = 30
icmp = socket.getprotobyname('icmp')
udp = socket.getprotobyname('udp')
ttl = 1
while True:
recv_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp) # [2]
send_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, udp)
send_socket.setsockopt(socket.SOL_IP, socket.IP_TTL, ttl) # [3]
recv_socket.bind(("", port)) # [4]
send_socket.sendto("", (dest_name, port)) # [5]
curr_addr = None
curr_name = None
try:
_, curr_addr = recv_socket.recvfrom(512)
curr_addr = curr_addr[0]
try:
curr_name = socket.gethostbyaddr(curr_addr)[0]
except socket.error:
curr_name = curr_addr
except socket.error:
pass
finally:
send_socket.close()
recv_socket.close()
if curr_addr is not None:
curr_host = "%s (%s)" % (curr_name, curr_addr)
else:
curr_host = "*"
print "%d\t%s" % (ttl, curr_host)
ttl += 1
if curr_addr == dest_addr or ttl > max_hops:
break
if __name__ == "__main__":
host = sys.argv[1]
main(host)
Jak widzimy, wykorzystane zostały wszystkie poznane do tej pory mechanizmy socketów — odczytanie adersu IP z nazwy domenowej [1], utworzenie gniazd korzystających z protokołu ICMP (do wysyłania pakietów) oraz UDP [2], ustawianie opcji TTL (Time To Live) dla wysyłanych pakietów [3] (o tym akurat nie mówiliśmy, ale opis funkcji setsockopt()można znaleźć w dokumentacji modułu socket — socket.setsockopt(level, optname, value)).
Jak widać, dzięki bardzo dużej elastyczności modułu socket, jesteśmy w stanie operować na różnych typach gniazd [4] czy kierunku komunikacji [5], nie jesteśmy także ograniczeni tylko przez jeden protokół.
Zakończenie
Przedstawione w artykule zagadnienia to tak naprawdę tylko czubek czubka góry lodowej, jaką jest programowanie (nie tylko w Pythonie i nie tylko sieciowe). Niewątpliwą zaletą opanowania mechanizmu socketów i sposobu pisania programów, które z nich korzystają, jest fakt, że implementacje w innych językach nie różnią się zbytnio (np. C.C++), oczywiście w ogólnych założeniach. To niewątpliwie bardzo ułatwia choćby przenoszenie kodu pomiędzy różnymi platformami.
Źródła
- Learning by doing: Writing your own traceroute in 8 easy steps | Oracle Blogs
- 17.2. socket — Low-level networking interface | The Python Standard Library
- Socket Programming HOWTO | Python HOWTOs
Rafał 'bl4de’ Janicki– bloorq[at]gmail.com

„(…) implementacje w innych językach nie różnią się zbytnio (np. C.C++), [b]oczwyiście[/b] w ogólnych założeniach.”
Tak, oczywiście chodzi o samą koncepcję (tworzenie gniazd, otwieranie połączeń, zapis do/odczyt z gniazd), nie o konkretną implementację w danym języku :)
Trochę niefortunnie brzmi część o „trzech rodzajach pakietów”. Chyba raczej chodzi o trzy rodzaje (lub idąc za dokumentacją typy) gniazd: dla pakietów TCP, UDP i bez wskazania konkretnego protokołu warstwy transportowej.
@MateuszM
Tak, dokładnie o to chodzi.
Nie lubię się powtarzać, ale…
Ruby!
;)
Zabiliście pythona – rozszerzcie trochę tą ostatnią ramkę z kodem źródłowym.
Obejście – zmniejszona czcionka :)
Po skopiowaniu i wklejeniu do edytora (Sublime Text) formatowanie jest zachowane :)
Świetny artykuł.
Pozdrawiam! L.
Świetna robota! Bardzo pomocny artykuł.