Comment puis-je compter chaque paquet UDP envoyé par des sous-processus?

J’ai une application Python qui orchestre les appels à un processus sous-jacent. Les processus sont appelés à l’aide de subprocess.check_output et effectuent des appels SNMP vers des périphériques réseau distants.

Pour surveiller les performances, je voudrais compter le nombre de paquets SNMP envoyés qui sont transmis. Je suis principalement intéressé par le nombre de paquets. La taille des paquets de demande / réponse serait également intéressante, mais moins importante. L’objective est d’avoir une idée du stress du pare-feu provoqué par cette application.

Donc, par souci d’argument, supposons l’application idiote suivante:

 from subprocess import check_output output = check_output(['snmpget', '-v2c', '-c', 'private', '192.168.1.1', '1.3.6.1.2.1.1.2.0']) print(output) 

Cela provoquerait l’envoi d’un nouveau paquet UDP sur le port 161.

Comment puis-je les compter dans un tel cas?

Voici une autre version avec des fonctions écrasées (pourrait également être un gestionnaire de contexte):

 from subprocess import check_call def start_monitoring(): pass def stop_monitoring(): return 0 start_monitoring() check_call(['snmpget', '-v2c', '-c', 'private', '192.168.1.1', '1.3.6.1.2.1.1.2.0']) check_call(['snmpget', '-v2c', '-c', 'private', '192.168.1.1', '1.3.6.1.2.1.1.2.0']) check_call(['snmpget', '-v2c', '-c', 'private', '192.168.1.1', '1.3.6.1.2.1.1.2.0']) num_connections = stop_monitoring() assert num_connections == 3 

Dans cet exemple artificiel, il y aura évidemment 3 appels, car j’exécute manuellement les appels SNMP. Mais dans l’exemple pratique, le nombre d’appels SNMP n’est pas égal aux appels au sous-processus. Parfois, un ou plusieurs GET sont exécutés, parfois il s’agit de simples passages (c’est-à-dire de nombreuses requêtes UDP séquentielles), parfois il y a des déplacements en masse (un nombre inconnu de requêtes).

Donc, je ne peux pas simplement surveiller le nombre de fois que l’application est appelée. Je dois vraiment surveiller les requêtes UDP.

Est-ce que quelque chose comme ça est même possible? Si oui, comment?

Il est probablement important de savoir que cela fonctionne sous Linux en tant qu’utilisateur non root. Mais tous les sous-processus s’exécutent en tant que même utilisateur.

Cela pourrait vous aider:

https://sourceware.org/systemtap/examples/

Le tcpdumplike.stp imprime une ligne pour chaque paquet TCP et UDP reçu. Chaque ligne comprend les adresses IP source et de destination, les ports source et de destination et les indicateurs.

Le code n’est pas écrit en python mais la conversion n’est pas une grosse affaire.

Suite à cette réponse , ce repository github via une autre réponse , je suis venu avec l’implémentation suivante d’un proxy / relais UDP:

 #!/usr/bin/env python from collections import namedtuple from contextlib import contextmanager from random import randint from time import sleep import logging import socket import threading import snmp MSG_DONTWAIT = 0x40 # from socket.h LOCK = threading.Lock() MSG_TYPE_REQUEST = 1 MSG_TYPE_RESPONSE = 2 Statistics = namedtuple('Statistics', 'msgtype packet_size') def visible_octets(data: bytes) -> str: """ Returns a geek-friendly (hexdump) output of a bytes object. Developer note: This is not super performant. But it's not something that's supposed to be run during normal operations (mostly for testing and debugging). So performance should not be an issue, and this is less obfuscated than existing solutions. Example:: >>> from os import urandom >>> print(visible_octets(urandom(40))) 99 1f 56 a9 25 50 f7 9b 95 7e ff 80 16 14 88 c5 ..V.%P...~...... f3 b4 83 d4 89 b2 34 b4 71 4e 5a 69 aa 9f 1d f8 ......4.qNZi.... 1d 33 f9 8e f1 b9 12 e9 .3...... """ from binascii import hexlify, unhexlify hexed = hexlify(data).decode('ascii') tuples = [''.join((a, b)) for a, b in zip(hexed[::2], hexed[1::2])] line = [] output = [] ascii_column = [] for idx, octet in enumerate(tuples): line.append(octet) # only use printable characters in ascii output ascii_column.append(octet if 32 <= int(octet, 16) < 127 else '2e') if (idx+1) % 8 == 0: line.append('') if (idx+1) % 8 == 0 and (idx+1) % 16 == 0: raw_ascii = unhexlify(''.join(ascii_column)) raw_ascii = raw_ascii.replace(b'\\n z', b'.') ascii_column = [] output.append('%-50s %s' % (' '.join(line), raw_ascii.decode('ascii'))) line = [] raw_ascii = unhexlify(''.join(ascii_column)) raw_ascii = raw_ascii.replace(b'\\n z', b'.') output.append('%-50s %s' % (' '.join(line), raw_ascii.decode('ascii'))) line = [] return '\n'.join(output) @contextmanager def UdpProxy(remote_host, remote_port, queue=None): thread = UdpProxyThread(remote_host, remote_port, stats_queue=queue) thread.prime() thread.start() yield thread.local_port thread.stop() thread.join() class UdpProxyThread(threading.Thread): def __init__(self, remote_host, remote_port, stats_queue=None): super().__init__() self.local_port = randint(60000, 65535) self.remote_host = remote_host self.remote_port = remote_port self.daemon = True self.log = logging.getLogger('%s.%s' % ( __name__, self.__class__.__name__)) self.running = True self._socket = None self.stats_queue = stats_queue def fail(self, reason): self.log.debug('UDP Proxy Failure: %s', reason) self.running = False def prime(self): """ We need to set up a socket on a FREE port for this thread. Retry until we find a free port. This is used as a separate method to ensure proper locking and to ensure that each thread has it's own port The port can be retrieved by accessing the *local_port* member of the thread. """ with LOCK: while True: try: self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._socket.bind(('', self.local_port)) break except OSError as exc: self.log.warning('Port %d already in use. Shuffling...', self.local_port) if exc.errno == 98: # Address already in use self.local_port = randint(60000, 65535) self._socket.close() else: raise @property def name(self): return 'UDP Proxy Thread {} -> {}:{}'.format(self.local_port, self.remote_host, self.remote_port) def start(self): if not self._socket: raise ValueError('Socket was not set. Call prime() first!') super().start() def run(self): try: known_client = None known_server = (self.remote_host, self.remote_port) self.log.info('UDP Proxy set up: %s -> %s:%s', self.local_port, self.remote_host, self.remote_port) while self.running: try: data, addr = self._socket.recvfrom(32768, MSG_DONTWAIT) self.log.debug('Packet received via %s\n%s', addr, visible_octets(data)) except BlockingIOError: sleep(0.1) # Give self.stop() a chance to sortinggger else: if known_client is None: known_client = addr if addr == known_client: self.log.debug('Proxying request packet to %s\n%s', known_server, visible_octets(data)) self._socket.sendto(data, known_server) if self.stats_queue: self.stats_queue.put(Statistics( MSG_TYPE_REQUEST, len(data))) else: self.log.debug('Proxying response packet to %s\n%s', known_client, visible_octets(data)) self._socket.sendto(data, known_client) if self.stats_queue: self.stats_queue.put(Statistics( MSG_TYPE_RESPONSE, len(data))) self.log.info('%s stopped!', self.name) finally: self._socket.close() def stop(self): self.log.debug('Stopping %s...', self.name) self.running = False if __name__ == '__main__': logging.basicConfig(level=0) from queue import Queue stat_queue = Queue() with UdpProxy('192.168.1.1', 161, stat_queue) as proxied_port: print(snmp.get('1.3.6.1.2.1.1.2.0', '127.0.0.1:%s' % proxied_port, 'testing')) with UdpProxy('192.168.1.1', 161, stat_queue) as proxied_port: print(snmp.get('1.3.6.1.2.1.1.2.0', '127.0.0.1:%s' % proxied_port, 'testing')) while not stat_queue.empty(): stat_item = stat_queue.get() print(stat_item) stat_queue.task_done() 

Comme vu dans la section __main__ , il peut simplement être utilisé comme suit:

  from queue import Queue stat_queue = Queue() with UdpProxy('192.168.1.1', 161, stat_queue) as proxied_port: print(snmp.get('1.3.6.1.2.1.1.2.0', '127.0.0.1:%s' % proxied_port, 'testing')) while not stat_queue.empty(): stat_item = stat_queue.get() print(stat_item) stat_queue.task_done() 

Une chose à noter: dans ce cas, le module snmp exécute simplement un subprocess.check_output()subprocess.check_output() pour générer un sous-processus snmpget .

Vous pouvez écrire une autre application python qui se connecte à la carte réseau locale en mode espion. Dans ce mode, vous verrez tout le trafic passant par votre adaptateur réseau. Et puis vous pouvez filtrer le trafic UDP à partir d’eux et finalement filtrer les paquets qui ont un port source 161. Mais cela doit s’exécuter en tant qu’utilisateur root ou privilégié.

 import socket # the public network interface HOST = socket.gethostbyname(socket.gethostname()) # create a raw socket and bind it to the public interface s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP) s.bind((HOST, 0)) # Include IP headers s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) # receive all packages s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON) # receive a package print s.recvfrom(65565) # disabled promiscuous mode s.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF) 

La source

Juste une pensée, mais vous pouvez utiliser un proxy UDP et simplement compter les messages envoyés / reçus par le proxy. Un proxy simple est déjà disponible sur SO: solution de proxy udp simple . Il serait sortingvial d’append des jetons aux solutions présentées ici.

Si vous devez les exécuter en parallèle, utilisez simplement des ports différents pour chaque processus. Sinon, vous pouvez simplement réutiliser le même port proxy pour chaque nouveau processus.

Malheureusement, puisque vous signalez que vous n’avez pas access à la configuration du pare-feu (je suppose que vous voulez dire que le pare-feu local – netfilter existe, qu’il soit utilisé ou non), vous disposez de très peu d’options.

Il y a quelques options de ligne de commande que vous pourriez rechercher sur la page de manuel snmpcmd. Notamment, l’option -d videra le contenu de chaque paquet envoyé et sera quelque peu désagréable à parsingr, mais au moins il est là (même si la sortie de l’adresse est incorrecte sur certaines plates-formes). Franchement, c’est probablement votre meilleur pari en tenant compte de vos exigences.

Il y a aussi (et ce sera très désagréable) la possibilité de griffonner un proxy UDP pour agir simplement comme intermédiaire entre les différents utilitaires SNMP, en relayant (après avoir compté) chaque paquet vous-même. Aucun privilège élevé ne sera nécessaire pour cela, car le protocole SNMP ne nécessite pas de port source de 161 – il est entièrement satisfait de répondre aux requêtes provenant de tout port haut que vous lui envoyez. Ainsi, vous pouvez vous connecter à n’importe quel port de l’interface locale que vous souhaitez, et pointer votre outil SNMP sur ce port, le proxy ne faisant que répéter tout ce qu’il voit dans la “vraie” destination.

Honnêtement, à moins que le pare-feu distant ne fonctionne sur une pomme de terre obsolète, les tâches de pare-feu ne constitueront jamais une charge CPU importante à moins que vous n’ayez littéralement des milliers de règles.

snmpget en particulier vous permet de passer -d flag, en consignant les paquets envoyés à stdout (peut être redirigé).

 $ snmpget -d -v2c -c private 192.168.1.1 1.3.6.1.2.1.1.2.0 No log handling enabled - using stderr logging Sending 44 bytes to UDP: [192.168.1.1]:161->[0.0.0.0]:0 . . . Timeout: No Response from 192.168.1.1. 

Si vos processus testés ne sont pas sous votre contrôle, je préférerais utiliser quelque chose sans codage: tcpdump ou strace . D’après mon expérience, il est possible de demander une entrée limitée aux utilisateurs de votre système sysops / devops lorsque les arguments de l’appel / le but est très limité.

Si les modifications de sudoers sont absolues non-non, vous pouvez diriger votre application testée vers une application proxy conçue à la main, configurée pour envoyer les paquets plus avant et qui fera le comptage.