*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

пятница, 4 сентября 2009 г.

Мультипотоковый отладчик TCP/IP соединений

Трассировка данных, передаваемых по TCP/IP, является весьма частой задачей при разработке сетевых приложений, особенно низкого уровня.

Программ для данной задачи существует превеликое множество. Но лично я очень давно использую для этих целей свой собственный велосипед. Причин тут несколько. Основная - мне нужна одна программа, одинаково работающая на многих платформах, включая даже Windows. Вторая по значимости причина - возможность налету что-то подкручивать, допиливать, вставлять миникуски кода для анализа конкретного протокола и т.д. Получается, что скриптовой язык тут является хорошим подспорьем.

Несколько лет назад первые версии моей утилиты были на PHP, но текущая версия переписана на Питоне.

Итак, pyspy.

Исходник небольшой, а, как мне кажется, разглядывание исходников должно радовать большинство программистов, особенно, если есть что покритиковать, поэтому приведу его прямо здесь (см. ниже).

Ни разу не претендую на оптимальность или крутизну использования Питона, поэтому принимаю любую критику.

Основные особенности и возможности:
  • программа "слушает" на указанном порту и перенаправляет траффик на указанные адрес и порт
  • умеет сохранять лог в файл
  • программа является многопотоковой, то есть может принимать сразу несколько входящих содинений
  • механизм записи лога работает также в отдельном потоке, ускоряет работу
Пример использования (для работы по Windows есть специальный скрипт pyspy.cmd).

Запускаем сервер:
pyspy.cmd -a 10.44.5.138 -p 5467 -l 9999 -L trace.log
Запускаем клиента:
telnet localhost 9999
и вводим GET / HTTP/1.0<ENTER><ENTER>

В файле лога и в консоли получаем вот такое:
0000: Listen at port 9999, remote host ('10.44.5.138', 5467)
0000: Connection accepted from ('127.0.0.1', 15223), thread 1 launched
0001: Thread started
0001: Connecting to ('10.44.5.138', 5467)...
0001: Remote host: ('127.0.0.1', 15223)
0001: Recevied from ('127.0.0.1', 15223) (1)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 47 | G
0001: Sent to ('10.44.5.138', 5467) (1)
0001: Recevied from ('127.0.0.1', 15223) (13)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 45 54 20 2F 20 48 54 54 50 2F 31 2E 30 | ET / HTTP/1.0
0001: Sent to ('10.44.5.138', 5467) (13)
0001: Recevied from ('127.0.0.1', 15223) (2)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 0D 0A | ..
0001: Sent to ('10.44.5.138', 5467) (2)
0001: Recevied from ('127.0.0.1', 15223) (2)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 0D 0A | ..
0001: Sent to ('10.44.5.138', 5467) (2)
0001: Recevied from ('10.44.5.138', 5467) (379)
0001: ----: 00-01-02-03-04-05-06-07-08-09-0A-0B-0C-0D-0E-0F
0001: ------------------------------------------------
0001: 0000: 48 54 54 50 2F 31 2E 31 20 33 30 32 20 46 6F 75 | HTTP/1.1 302 Fou
0001: 0010: 6E 64 0D 0A 44 61 74 65 3A 20 46 72 69 2C 20 30 | nd..Date: Fri, 0
0001: 0020: 34 20 53 65 70 20 32 30 30 39 20 30 38 3A 35 33 | 4 Sep 2009 08:53
0001: 0030: 3A 30 33 20 47 4D 54 0D 0A 53 65 72 76 65 72 3A | :03 GMT..Server:
0001: 0040: 20 41 70 61 63 68 65 0D 0A 50 72 61 67 6D 61 3A | Apache..Pragma:
0001: 0050: 20 6E 6F 2D 63 61 63 68 65 0D 0A 45 78 70 69 72 | no-cache..Expir
0001: 0060: 65 73 3A 20 46 72 69 2C 20 30 31 20 4A 61 6E 20 | es: Fri, 01 Jan
0001: 0070: 31 39 39 39 20 30 30 3A 30 30 3A 30 30 20 47 4D | 1999 00:00:00 GM
0001: 0080: 54 0D 0A 43 61 63 68 65 2D 63 6F 6E 74 72 6F 6C | T..Cache-control
0001: 0090: 3A 20 6E 6F 2D 63 61 63 68 65 2C 20 6E 6F 2D 63 | : no-cache, no-c
0001: 00A0: 61 63 68 65 3D 22 53 65 74 2D 43 6F 6F 6B 69 65 | ache="Set-Cookie
0001: 00B0: 22 2C 20 70 72 69 76 61 74 65 0D 0A 4C 6F 63 61 | ", private..Loca
...
[обрезано]
...
0001: 0100: 76 3D 31 0D 0A 43 6F 6E 6E 65 63 74 69 6F 6E 3A | v=1..Connection:
0001: 0110: 20 63 6C 6F 73 65 0D 0A 43 6F 6E 74 65 6E 74 2D | close..Content-
0001: 0120: 54 79 70 65 3A 20 74 65 78 74 2F 68 74 6D 6C 0D | Type: text/html.
0001: 0130: 0A 0D 0A 52 65 64 69 72 65 63 74 20 70 61 67 65 | ...Redirect page
0001: 0140: 3C 62 72 3E 3C 62 72 3E 0A 54 68 65 72 65 20 69 | <br><br>.There i
0001: 0150: 73 20 6E 6F 74 68 69 6E 67 20 74 6F 20 73 65 65 | s nothing to see
0001: 0160: 20 68 65 72 65 2C 20 70 6C 65 61 73 65 20 6D 6F | here, please mo
0001: 0170: 76 65 20 61 6C 6F 6E 67 2E 2E 2E | ve along...
0001: Sent to ('127.0.0.1', 15223) (379)
0001: Connection reset by ('10.44.5.138', 5467)
0001: Connection closed
Теперь, собственно, исходник:
#!/usr/bin/python

import socket, string, threading, os, select, sys, time, getopt
from sys import argv

def usage():
name = os.path.basename(argv[0])
print "usage:", name, "-l listen_port -a host -p port [-L file] [-c] [-h?]"
print " -a host - address/host to connect"
print " -p port - remote port to connect"
print " -l listen_port - local port to listen"
print " -L file - log file"
print " -c - supress console output"
print " -h or -? - this help"
print " -v - version"
sys.exit(1)

PORT = False
REMOTE_HOST = REMOTE_PORT = False

CONSOLE = True
LOGFILE = False

try:
opts, args = getopt.getopt(argv[1:], "l:a:p:L:ch?v")

for opt in opts:
opt, val = opt
if opt == "-l":
PORT = int(val)
elif opt == "-a":
REMOTE_HOST = val
elif opt == "-p":
REMOTE_PORT = int(val)
elif opt == "-L":
LOGFILE = val
elif opt == "-c":
CONSOLE = False
elif opt == "-?" or opt == "-h":
usage()
elif opt == "-v":
print "Python TCP/IP Spy Version 1.01 Copyright (c) 2009 by Alexander Demin"
sys.exit(1)
else:
usage()

if not PORT:
raise StandardError, "listen port is not given"

if not REMOTE_HOST:
raise StandardError, "remote host is not given"

if not REMOTE_PORT:
raise StandardError, "remote port is not given"

except Exception, e:
print "error:", e, "\n"
usage()

# Remote host
REMOTE = (REMOTE_HOST, REMOTE_PORT)

# Create logging contitional variable
log_cond = threading.Condition()

queue = []

def logger():
global queue
while 1:
log_cond.acquire()

while len(queue) == 0:
log_cond.wait()

if LOGFILE:
try:
logfile = open(LOGFILE, "a+")
logfile.writelines(map(lambda x: x+"\n", queue))
logfile.close()
except: pass

if CONSOLE:
for line in queue:
print line

queue = []
log_cond.release()

# Thread safe logger
def log(thread, msg):
if CONSOLE or LOGFILE:
log_cond.acquire()
queue.append("%04d: %s" % (thread, msg))
log_cond.notify()
log_cond.release()

def printable(ch):
return (int(ch < 32) and '.') or (int(ch >= 32) and chr(ch))

# Pre-build a printable characters map
printable_map = [ printable(x) for x in range(256) ]

# Thread safe dumper
def log_dump(thread, msg):

if CONSOLE or LOGFILE:
log_cond.acquire()

width = 16

header = reduce(lambda x, y: x + ("%02X-" % y), range(width), "")[0:-1]
queue.append("%04d: ----: %s" % (thread, header))
queue.append("%04d: %s" % (thread, '-' * width * 3))

i = 0
while 1:
line = msg[i:i+width]
if len(line) == 0: break
dump = reduce(lambda x, y: x + ("%02X " % ord(y)), line, "")
char = reduce(lambda x, y: x + printable_map[ord(y)], line, "")
queue.append("%04X: %04X: %-*s| %-*s" % (thread, i, width*3, dump, width, char))
i = i + width

log_cond.notify()
log_cond.release()

# Spy thread
def spy_thread(local, addr, thread_id):
log(thread_id, "Thread started")

try:
log(thread_id, "Connecting to %s..." % str(REMOTE))
remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
remote.connect(REMOTE)
except Exception, e:
log(thread_id, "Unable connect to %s -> %s" % (REMOTE, e))
local.close()
return

LOCAL = str(addr)

log(thread_id, "Remote host: " + LOCAL)

try:
running = 1;
while running == 1:

rd, wr, er = select.select([local, remote], [], [local, remote], 3600)

for sock in er:
if sock == local:
log(thread_id, "Connection error from " + LOCAL)
running = 0
if sock == remote:
log(thread_id, "Connection error from " + REMOTE)
running = 0

for sock in rd:
if sock == local:
val = local.recv(1024)
if val:
log(thread_id, "Recevied from %s (%d)" % (LOCAL, len(val)))
log_dump(thread_id, val)
remote.send(val)
log(thread_id, "Sent to %s (%d)" % (REMOTE, len(val)))
else:
log(thread_id, "Connection reset by %s" % LOCAL)
running = 0;

if sock == remote:
val = remote.recv(1024)
if val:
log(thread_id, "Recevied from %s (%d)" % (REMOTE, len(val)))
log_dump(thread_id, val)
local.send(val)
log(thread_id, "Sent to %s (%d)" % (LOCAL, len(val)))
else:
log(thread_id, "Connection reset by %s" % str(REMOTE))
running = 0;

except Exception, e:
log(thread_id, ("Connection terminated: " + str(e)))

remote.close()
local.close()

log(thread_id, "Connection closed")

try:
# Server socket
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.bind(("", PORT))
except Exception, e:
print "error", e
sys.exit(1)

counter = 1

threading.Thread(target=logger, args=[]).start()

log(0, "Listen at port %d, remote host %s" % (PORT, REMOTE))

while 1:
srv.listen(1)
local, addr = srv.accept()
log(0, "Connection accepted from %s, thread %d launched" % (addr, counter))
threading.Thread(target=spy_thread, args=[local, addr, counter]).start()
counter = counter + 1
Лично я постоянно использую этот скрипт на Windows, Linux и Solaris.

Следующий шаг - это переписать все на чистом С в виде одного единственного файла, который можно было бы в течение минуты забросить на любой UNIX или Windows, скомпилить и получить готовую программу. Питон - это конечно здорово, но, например, для AIX или HP-UX Питон является небольшой загвоздкой, которую в пять секунд не решить.

А что стоит у вас на вооружении по этому вопросу?

3 комментария:

  1. Wireshark http://www.wireshark.org/
    Быстро, просто, удобно + есть парсеры для множества протоколов.

    Да и tcpdump никто не отменял :)

    ОтветитьУдалить
  2. Начал было качать Wireshark portable под винды. А она мне и говорит "Перевед! Медвед!" - 18 мегов!. А почему не 100 или не гиг? Места, конечно, хватит, но какой-то осадок появляется :-). А если серьезно, то для сниффинга/анализа готовых протоколов - все хорошо, есть много программ, и это одна из них. Но обычно косяки и неудобства начинаются при работе над чем-то своим, нестандартным.

    А разве tcpdump умеет несколько соединений одновременно трейсить?...

    ОтветитьУдалить