From 06460fb7692affb9cb652acb2d5290d8d7f8f261 Mon Sep 17 00:00:00 2001
From: gpotter2 <10530980+gpotter2@users.noreply.github.com>
Date: Wed, 30 Apr 2025 15:11:13 +0200
Subject: [PATCH 1/9] New: ForwardingMachine
---
doc/scapy/fwdmachine.rst | 88 +++++
doc/scapy/graphics/fwdmachine.drawio | 135 ++++++++
doc/scapy/graphics/fwdmachine.svg | 3 +
scapy/fwdmachine.py | 482 +++++++++++++++++++++++++++
scapy/layers/tls/cert.py | 15 +
5 files changed, 723 insertions(+)
create mode 100644 doc/scapy/fwdmachine.rst
create mode 100644 doc/scapy/graphics/fwdmachine.drawio
create mode 100644 doc/scapy/graphics/fwdmachine.svg
create mode 100644 scapy/fwdmachine.py
diff --git a/doc/scapy/fwdmachine.rst b/doc/scapy/fwdmachine.rst
new file mode 100644
index 00000000000..ce8b3ba1a3e
--- /dev/null
+++ b/doc/scapy/fwdmachine.rst
@@ -0,0 +1,88 @@
+******************
+Forwarding Machine
+******************
+
+Scapy's ``ForwardingMachine`` is a class utility that allows to very quickly design a server that forwards packets to another server, with the ability
+to modify them on-the-fly. This is commonly referred to as a "transparent proxy". The ``ForwardingMachine`` was initially designed to be used with TPROXY,
+a linux feature that allows to bind a socket that received *packets to any IP destination*, in which case it properly forwards the packet to the initially
+intended destination.
+
+A ``ForwardingMachine`` is expected to be used over a normal Python socket, of any kind, and needs to extended with two
+functions: ``xfrmcs`` and ``xfrmsc``. The first one is called whenever data is received from the client side (client-to-server), the other when the data
+is received from the server.
+
+Basic usage
+___________
+
+Here's an example of a ``ForwardingMachine`` over TPROXY that does nothing. Packets for all destinations are handled, and forwarded to their
+initial destinations afterwards. More details on how to setup TPROXY are provided below. Note that a ``ForwardingMachine`` **also works without TPROXY**.
+
+.. code:: python
+
+ from scapy.fwdmachine import ForwardMachine
+ from scapy.layers.http import HTTP
+
+ class HTTPEdit(ForwardMachine):
+ def xfrmcs(self, pkt, ctx):
+ pkt.show() # we print the client->server packets
+ raise self.FORWARD()
+
+ def xfrmsc(self, pkt, ctx):
+ pkt.show() # we print the server->client packets
+ raise self.FORWARD()
+
+ # Run it
+ HTTPEdit(
+ mode=ForwardMachine.MODE.TPROXY,
+ port=80,
+ cls=HTTP, # we specify the class of the payload we are receiving
+ ).run()
+
+The callback classes use **Operations** to tell the ``ForwardingMachine`` what to do with the incoming data.
+
+.. figure:: ../graphics/fwdmachine.svg
+ :align: center
+
+ The main operations available in a Forwarding machine, in this case in ``xfrmcs``.
+
+There are currently 5 operations available:
+
+- **FORWARD**: forward the received payload to the destination intended by the peer;
+- **FORWARD_REPLACE**: forward a modified payload to the intended destination;
+- **DROP**: drop the received payload;
+- **ANSWER**: answer the peer directly with a payload, without forwarding its original payload to the other peer;
+- **REDIRECT_TO**: (client-side only) redirects the connection of the client towards a new remote peer.
+
+The ``ctx`` attribute in the callbacks contains context relative to the current client. It can also be use to
+store additional data specific to the session.
+
+TLS support
+___________
+
+``ForwardingMachine`` has support for TLS through the ``ssl=True`` argument. When TLS is enabled, the SNI (Server Name Indication) is
+properly forwarded to the remote peer, and can be accessed through the ``ctx.tls_sni_name`` attribute in the callbacks.
+
+**By default, a ``ForwardingMachine`` generates self-signed certificates** that copy the attributes from the certificate of the remote
+server. This behavior can be changed by specifying a certificate (which will be served by the TLS stack).
+
+.. code:: python
+
+ from scapy.fwdmachine import ForwardMachine
+ from scapy.layers.http import HTTP
+
+ class HTTPSDump(ForwardMachine):
+ def xfrmcs(self, pkt, ctx):
+ pkt.show() # we print the client->server packets
+ raise self.FORWARD()
+
+ def xfrmsc(self, pkt, ctx):
+ pkt.show() # we print the server->client packets
+ raise self.FORWARD()
+
+ # Run it
+ HTTPSDump(
+ mode=ForwardMachine.MODE.TPROXY,
+ port=443,
+ cls=HTTP,
+ ssl=True,
+ ).run()
\ No newline at end of file
diff --git a/doc/scapy/graphics/fwdmachine.drawio b/doc/scapy/graphics/fwdmachine.drawio
new file mode 100644
index 00000000000..bb9caacaadd
--- /dev/null
+++ b/doc/scapy/graphics/fwdmachine.drawio
@@ -0,0 +1,135 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/scapy/graphics/fwdmachine.svg b/doc/scapy/graphics/fwdmachine.svg
new file mode 100644
index 00000000000..4cdf6dd1608
--- /dev/null
+++ b/doc/scapy/graphics/fwdmachine.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/scapy/fwdmachine.py b/scapy/fwdmachine.py
new file mode 100644
index 00000000000..134b8d13c01
--- /dev/null
+++ b/scapy/fwdmachine.py
@@ -0,0 +1,482 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# This file is part of Scapy
+# See https://scapy.net/ for more information
+# Copyright (C) Gabriel Potter
+
+"""
+Forwarding machine.
+"""
+
+from datetime import datetime, timezone, timedelta
+
+import enum
+import functools
+import os
+import select
+import socket
+import ssl
+import sys
+import threading
+import traceback
+
+from scapy.asn1.asn1 import ASN1_BOOLEAN, ASN1_OID, ASN1_UTC_TIME
+from scapy.config import conf
+from scapy.data import MTU
+from scapy.packet import Packet
+from scapy.supersocket import StreamSocket, SSLStreamSocket
+from scapy.themes import DefaultTheme, ColorOnBlackTheme
+from scapy.utils import get_temp_file
+from scapy.volatile import RandInt
+
+from scapy.layers.tls.all import (
+ Cert,
+ Chain,
+ PrivKeyRSA,
+ PrivKeyECDSA,
+)
+from scapy.layers.x509 import (
+ X509_AlgorithmIdentifier,
+ X509_SubjectPublicKeyInfo,
+)
+
+from cryptography.hazmat.primitives import serialization
+
+# Typing imports
+from typing import (
+ Type,
+ Optional,
+)
+
+
+class ForwardMachine:
+ """
+ Forward Machine
+
+ This binds a port and relay any connections from 'clients' to
+ their original destination a 'server'. Forwarding machine can be used in
+ two modes:
+
+ - SERVER: the server binds a port on its local IP and forwards packets to a
+ `remote_ip`.
+ - TPROXY: the server binds can intercept packets to any IP destination, provided
+ that they are routed through the local server, and some tweaking of the OS
+ routes;
+
+ The TPROXY mode is expected to be used on a router with FORWARDING and only a specific
+ set of *nat rules set to -j TPROXY. A scripts called 'vethrelay.sh' is provided in the documentation
+ for setting this up.
+
+ ForwardMachine supports transparently proxifying TLS. By default, it will generate lookalike self-signed
+ certificates, but it's also possible to specify a certificate by using crtfile and keyfile.
+
+ Parameters:
+
+ :param port: the port to listen on
+ :param cls: the scapy class to parse on that port
+ :param af: the address family to use (default AF_INET)
+ :param proto: the proto to use (default SOCK_STREAM)
+ :param remote_ip: the IP to use in SERVER mode, or by default in TPROXY when the destination
+ is the local IP.
+ :param remote_af: (optional) if provided, use a different address family to connect
+ to the remote host.
+ :param bind_ip: the IP to bind locally. "0.0.0.0" by default in SERVER mode, but "2.2.2.2" by default
+ in TPROXY (if you are using the provided 'vethrelay.sh' script).
+ :param tls: enable TLS (in both the server and client)
+ :param crtfile: (optional) if provided, uses a certificate instead of self signed ones.
+ :param keyfile: (optional) path to the key file
+ :param timeout: the timeout before connecting to the real server (default 2)
+
+ Methods to override:
+
+ :func xfrmcs: a function to call when forwarding a packet from the 'client' to
+ the server. If it returns True, the packet is forwarded as it. If it
+ returns False or None, the packet is discarded. If it returns a
+ packet, this packet is forwarded instead of the original packet.
+ :func xfrmsc: same as xfrmcs for packets forwarded from the 'server' to the
+ 'client'.
+ """
+
+ class MODE(enum.Enum):
+ SERVER = 0
+ TPROXY = 1
+
+ def __init__(
+ self,
+ mode: MODE,
+ port: int,
+ cls: Type[Packet],
+ af: socket.AddressFamily = socket.AF_INET,
+ proto: socket.SocketKind = socket.SOCK_STREAM,
+ remote_ip: str = None,
+ remote_af: Optional[socket.AddressFamily] = None,
+ bind_ip: str = None,
+ tls: bool = False,
+ crtfile: Optional[str] = None,
+ keyfile: Optional[str] = None,
+ timeout: int = 2,
+ MTU: int = MTU,
+ **kwargs,
+ ):
+ self.mode = mode
+ self.port = port
+ self.cls = cls
+ self.af = af
+ self.remote_af = remote_af or af
+ self.proto = proto
+ self.tls = tls
+ self.crtfile = crtfile
+ self.keyfile = keyfile
+ self.timeout = timeout
+ self.MTU = MTU
+ self.remote_ip = remote_ip
+ if self.tls:
+ self.sockcls = SSLStreamSocket
+ else:
+ self.sockcls = StreamSocket
+ # Chose 'bind_ip' depending on the mode
+ if self.mode == ForwardMachine.MODE.SERVER:
+ self.bind_ip = "0.0.0.0"
+ elif self.mode == ForwardMachine.MODE.TPROXY:
+ self.bind_ip = "2.2.2.2"
+ else:
+ raise ValueError("Unknown mode :/")
+ red = lambda z: functools.reduce(lambda x, y: x + y, z)
+ # Session settings
+ self.local_ips = red(red(list(x.ips.values())) for x in conf.ifaces.values())
+ self.cache = {}
+ super(ForwardMachine, self).__init__(**kwargs)
+
+ def run(self):
+ """
+ Function to start the relay server
+ """
+ self.ssock = socket.socket(self.af, self.proto, 0)
+ self.ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ if self.mode == ForwardMachine.MODE.TPROXY:
+ self.ssock.setsockopt(socket.SOL_IP, socket.IP_TRANSPARENT, 1) # TPROXY !
+ self.ssock.bind((self.bind_ip, self.port))
+ self.ssock.listen(5)
+ print(ct.green("Relay server waiting on port %s" % self.port))
+ while True:
+ conn, addr = self.ssock.accept()
+ # Calc dest
+ dest = conn.getsockname()
+ if dest[0] in self.local_ips and self.remote_ip:
+ dest = (self.remote_ip,) + dest[1:]
+ print(ct.green("%s -> %s connected !" % (repr(addr), repr(dest))))
+ try:
+ threading.Thread(
+ target=self.handler,
+ args=(conn, addr, dest),
+ ).start()
+ except Exception as ex:
+ print(ct.red("%s errored !" % repr(addr)))
+ conn.close()
+ pass
+
+ def xfrmcs(self, pkt, ctx):
+ """
+ DEV: overwrite me to handle client->server
+ """
+ raise self.FORWARD()
+
+ def xfrmsc(self, pkt, ctx):
+ """
+ DEV: overwrite me to handle server->client
+ """
+ raise self.FORWARD()
+
+ # Command Exceptions
+
+ class DROP(Exception):
+ # Drop this packet.
+ pass
+
+ class FORWARD(Exception):
+ # Forward this packet.
+ pass
+
+ class FORWARD_REPLACE(Exception):
+ # Replace the content and forward.
+ def __init__(self, data):
+ self.data = data
+
+ class ANSWER(Exception):
+ # Answer directly
+ def __init__(self, data):
+ self.data = data
+
+ class REDIRECT_TO(Exception):
+ # Redirect this socket to another destination
+ def __init__(self, host, port, then=None, server_hostname=None):
+ self.dest = (host, port)
+ self.server_hostname = server_hostname
+ self.then = then or ForwardMachine.FORWARD()
+
+ class CONTEXT:
+ """
+ CONTEXT object kept during a session
+ """
+
+ def __init__(self, addr, dest):
+ self.addr = addr
+ self.dest = dest
+ self.tls_sni_name = None # Retrieved when receiving a connection
+
+ def print_reply(self, evt, cs, req, rep):
+ if evt == self.FORWARD:
+ if cs:
+ print("C ==> S: %s" % req.summary())
+ else:
+ print("S ==> C: %s" % req.summary())
+ elif evt == self.FORWARD_REPLACE:
+ if cs:
+ print("C /=> S: %s -> %s" % (req.summary(), rep.summary()))
+ else:
+ print("S /=> C: %s -> %s" % (req.summary(), rep.summary()))
+ elif evt == self.DROP:
+ if cs:
+ print("C => 0: %s" % req.summary())
+ else:
+ print("S => 0: %s" % req.summary())
+ elif evt == self.ANSWER:
+ if cs:
+ print("C <=| : %s -> %s" % (req.summary(), rep.summary()))
+ else:
+ print("S <=| : %s -> %s" % (req.summary(), rep.summary()))
+
+ def destalias(self, dest):
+ """
+ Alias a destination to another destination.
+ A destination is the tuple (host, port)
+ """
+ return dest
+
+ def _getpeersock(self, dest, server_hostname=None):
+ """
+ Get peer socket
+ """
+ s = socket.socket(self.remote_af, self.proto)
+ s.settimeout(self.timeout)
+ ndest = self.destalias(dest)
+ if ndest != dest:
+ print("C: %s redirected to %s" % (repr(dest), repr(ndest)))
+ dest = ndest
+ s.connect(dest) # we connect to the real destination server
+ return s
+
+ def gen_alike_chain(self, certs, privkey):
+ """
+ Modify a real certificate chain to be served by our own privatekey
+ """
+ c, certs = certs[0], certs[1:]
+ if certs:
+ # Recursive: if there are certificates above this one in the chain, do them
+ # first.
+ certs = self.gen_alike_chain(certs, privkey)
+ else:
+ # Last certificate of the chain. Make it self-signed
+ c.tbsCertificate.issuer = c.tbsCertificate.subject
+ # Set SubjectPublicKeyInfo to the one from our private key
+ c.setSubjectPublicKeyFromPrivateKey(privkey)
+ # Filter out extensions that would cause trouble
+ c.tbsCertificate.serialNumber.val = int(RandInt()) # otherwise SEC_ERROR_REUSED_ISSUER_AND_SERIAL
+ c.tbsCertificate.extensions = [
+ x
+ for x in c.tbsCertificate.extensions
+ if x.extnID not in [
+ '2.5.29.32', # CPS
+ '2.5.29.31', # cRLDistributionPoints
+ '1.3.6.1.5.5.7.1.1', # authorityInfoAccess
+ '1.3.6.1.4.1.11129.2.4.2', # SCT
+ '2.5.29.14', # subjectKeyIdentifier
+ '2.5.29.35', # authorityKeyIdentifier
+ ]
+ ]
+ # For now, we only provide a RSA private key, so we can only sign with that :/
+ c.tbsCertificate.signature = X509_AlgorithmIdentifier(
+ algorithm=ASN1_OID("ecdsa-with-SHA384"),
+ )
+ # Resign.
+ c = Cert(privkey.resignCert(c))
+ # Return
+ return [c] + certs
+
+ def get_key_and_alike_chain(self, cas, dest, server_name):
+ """
+ Generate a PrivateKey and a clone of the 'cas' certificate chain signed with it,
+ if not already cached.
+
+ The cache uses server_name or dest as key.
+ """
+ ident = server_name or dest
+ if ident in self.cache:
+ return self.cache[ident]
+ # Parse CAs
+ certs = [Cert(c.public_bytes()) for c in cas]
+ # certs = certs[:1]
+ # Generate Private Key
+ privkey = PrivKeyECDSA()
+ # Iterate
+ certs = self.gen_alike_chain(certs, privkey)
+ # Build a chain object. This checks that everything is properly signed, and re-order
+ # the certs.
+ # chain = Chain(certs, cert0=certs[-1])
+ self.cache[ident] = privkey, certs
+ return privkey, certs
+
+
+ def handler(self, sock, addr, dest):
+ """
+ Handler of a client socket
+ """
+ ctx = self.CONTEXT(addr, dest) # we have a context object
+ # Initialize peer socket
+ ss = self._getpeersock(dest)
+ # Wrap both server and peer sockets in SSL
+ if self.tls:
+ # Build client SSL context
+ clisslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
+ clisslcontext.load_default_certs()
+ clisslcontext.check_hostname = False
+ clisslcontext.verify_mode = ssl.CERT_NONE
+
+ # This acts as follows:
+ # - start the server-side TLS handshake
+ # - use the SNI callback to pop a client-side socket (using the real provided SNI)
+ # - serve the certificate
+
+ _clisock = [ss]
+
+ def cb_sni(sock, server_name, _):
+ """
+ This callback occurs after the TLSClientHello is received by the server
+ """
+ ss = _clisock[0]
+ ctx.tls_sni_name = server_name # the requested SNI
+ # Use that SNI to wrap the client socket
+ ss = clisslcontext.wrap_socket(ss, server_hostname=server_name)
+ # Get certificate chain
+ cas = ss._sslobj.get_unverified_chain()
+ # Get negotiated cipher type
+ cipher = ss.shared_ciphers()
+ if self.crtfile is None:
+ # SELF-SIGNED mode
+ # Generate private key based on the type of certificate
+ privkey, certs = self.get_key_and_alike_chain(cas, dest, server_name)
+ # Load result certificate our SSL server
+ # (this is dumb but we need to store them on disk)
+ certfile = get_temp_file()
+ with open(certfile, "wb") as fd:
+ for c in certs:
+ fd.write(c.pem)
+ keyfile = get_temp_file()
+ with open(keyfile, "wb") as fd:
+ password = os.urandom(32)
+ fd.write(privkey.key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.BestAvailableEncryption(password),
+ ))
+ else:
+ # Certificate is provided
+ certfile = self.crtfile
+ keyfile = self.keyfile
+ sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
+ sslcontext.check_hostname = False
+ sslcontext.verify_mode = ssl.CERT_NONE # note: server side
+ sslcontext.load_cert_chain(certfile, keyfile, password=password)
+ sock.context = sslcontext
+ # Return success
+ _clisock[0] = ss
+ return None # Continue
+
+ # Server SSL context
+ sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
+ sslcontext.sni_callback = cb_sni
+ try:
+ sock = sslcontext.wrap_socket(sock, server_side=True)
+ except Exception as ex:
+ print(ct.red("%s errored in SSL: %s" % (repr(addr), str(ex))))
+ sock.close()
+ return
+ ss = _clisock[0]
+ # Wrap the sockets
+ sock = self.sockcls(sock, self.cls)
+ ss = self.sockcls(ss, self.cls)
+ try:
+ while True:
+ # Listen on both ends of the connection
+ for thissock in select.select([ss, sock], [], [], 0)[0]:
+ if thissock is ss:
+ cs = 0
+ func = self.xfrmsc
+ othersock = sock
+ else:
+ cs = 1
+ func = self.xfrmcs
+ othersock = ss
+ # get data
+ try:
+ data = thissock.recv(self.MTU)
+ except EOFError:
+ raise RuntimeError
+ if not data:
+ # Session needs more data
+ continue
+ try:
+ # And pipe everything into the processdata
+ try:
+ func(data, ctx)
+ # If this doesn't raise, it's a user error.
+ print(ct.red("%s ERROR: you must always raise in %s !" % func))
+ return
+ except self.REDIRECT_TO as ex:
+ # Replace the peer socket with a new socket
+ oldss = ss
+ ss = self._getpeersock(
+ ex.dest, server_hostname=ex.server_hostname
+ )
+ ss = self.sockcls(ss, self.cls)
+ print("C: %s redirected to %s" % (repr(ctx.dest), repr(ex.dest)))
+ ctx.dest = ex.dest # update context
+ # Shut the old one.
+ oldss.ins.shutdown(socket.SHUT_RDWR)
+ oldss.close()
+ # Replace othersock/thissock
+ if oldss is thissock:
+ thissock = ss
+ else:
+ othersock = ss
+ # Raise what's next.
+ raise ex.then
+ except self.FORWARD:
+ # Forward the data to the other host
+ othersock.send(data)
+ self.print_reply(self.FORWARD, cs, data, None)
+ except self.FORWARD_REPLACE as ex:
+ # Forward custom data to the other host
+ othersock.send(ex.data)
+ self.print_reply(self.FORWARD_REPLACE, cs, data, ex.data)
+ except self.DROP:
+ # Drop
+ self.print_reply(self.DROP, cs, data, None)
+ except self.ANSWER as ex:
+ # Respond with custom data
+ thissock.send(ex.data)
+ self.print_reply(self.ANSWER, cs, data, ex.data)
+ except Exception as ex:
+ # Processing failed. forward to not break anything
+ print(
+ ct.orange(
+ "Exception happend in handling client %s ! (forwarding.)"
+ % repr(addr)
+ )
+ )
+ traceback.print_exception(ex)
+ othersock.send(data)
+ self.print_reply(self.FORWARD, cs, data, None)
+ except RuntimeError:
+ print(ct.red("%s DISCONNECTED !" % repr(addr)))
+ sock.close()
+ ss.close()
diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py
index aa3b03febc8..58be5ce6438 100644
--- a/scapy/layers/tls/cert.py
+++ b/scapy/layers/tls/cert.py
@@ -777,6 +777,21 @@ def getSignatureHash(self):
h = hash_by_oid[sigAlg.algorithm.val]
return _get_hash(h)
+ def setSubjectPublicKeyFromPrivateKey(self, key):
+ """
+ Replace the subjectPublicKeyInfo of this certificate with the one from
+ the provided key.
+ """
+ if isinstance(key, (PubKey, PrivKey)):
+ self.tbsCertificate.subjectPublicKeyInfo = X509_SubjectPublicKeyInfo(
+ key.pubkey.public_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+ )
+ else:
+ raise ValueError("Unknown type 'key', should be PubKey or PrivKey")
+
def remainingDays(self, now=None):
"""
Based on the value of notAfter field, returns the number of
From 5e7842da0691de27c529d63bbb113d0813a83226 Mon Sep 17 00:00:00 2001
From: gpotter2 <10530980+gpotter2@users.noreply.github.com>
Date: Wed, 30 Apr 2025 23:15:38 +0200
Subject: [PATCH 2/9] Document forwarding machines, split Advanced Usage
---
doc/scapy/advanced_usage.rst | 1216 -----------------
doc/scapy/advanced_usage/asn1_snmp.rst | 485 +++++++
doc/scapy/advanced_usage/automaton.rst | 376 +++++
doc/scapy/{ => advanced_usage}/fwdmachine.rst | 19 +-
doc/scapy/advanced_usage/index.rst | 10 +
doc/scapy/advanced_usage/pipetools.rst | 347 +++++
doc/scapy/graphics/fwdmachine.svg | 2 +-
doc/scapy/index.rst | 2 +-
tox.ini | 3 +-
9 files changed, 1233 insertions(+), 1227 deletions(-)
delete mode 100644 doc/scapy/advanced_usage.rst
create mode 100644 doc/scapy/advanced_usage/asn1_snmp.rst
create mode 100644 doc/scapy/advanced_usage/automaton.rst
rename doc/scapy/{ => advanced_usage}/fwdmachine.rst (82%)
create mode 100644 doc/scapy/advanced_usage/index.rst
create mode 100644 doc/scapy/advanced_usage/pipetools.rst
diff --git a/doc/scapy/advanced_usage.rst b/doc/scapy/advanced_usage.rst
deleted file mode 100644
index 29053c10561..00000000000
--- a/doc/scapy/advanced_usage.rst
+++ /dev/null
@@ -1,1216 +0,0 @@
-**************
-Advanced usage
-**************
-
-ASN.1 and SNMP
-==============
-
-What is ASN.1?
---------------
-
-.. note::
-
- This is only my view on ASN.1, explained as simply as possible. For more theoretical or academic views, I'm sure you'll find better on the Internet.
-
-ASN.1 is a notation whose goal is to specify formats for data exchange. It is independent of the way data is encoded. Data encoding is specified in Encoding Rules.
-
-The most used encoding rules are BER (Basic Encoding Rules) and DER (Distinguished Encoding Rules). Both look the same, but the latter is specified to guarantee uniqueness of encoding. This property is quite interesting when speaking about cryptography, hashes, and signatures.
-
-ASN.1 provides basic objects: integers, many kinds of strings, floats, booleans, containers, etc. They are grouped in the so-called Universal class. A given protocol can provide other objects which will be grouped in the Context class. For example, SNMP defines PDU_GET or PDU_SET objects. There are also the Application and Private classes.
-
-Each of these objects is given a tag that will be used by the encoding rules. Tags from 1 are used for Universal class. 1 is boolean, 2 is an integer, 3 is a bit string, 6 is an OID, 48 is for a sequence. Tags from the ``Context`` class begin at 0xa0. When encountering an object tagged by 0xa0, we'll need to know the context to be able to decode it. For example, in SNMP context, 0xa0 is a PDU_GET object, while in X509 context, it is a container for the certificate version.
-
-Other objects are created by assembling all those basic brick objects. The composition is done using sequences and arrays (sets) of previously defined or existing objects. The final object (an X509 certificate, a SNMP packet) is a tree whose non-leaf nodes are sequences and sets objects (or derived context objects), and whose leaf nodes are integers, strings, OID, etc.
-
-Scapy and ASN.1
----------------
-
-Scapy provides a way to easily encode or decode ASN.1 and also program those encoders/decoders. It is quite laxer than what an ASN.1 parser should be, and it kind of ignores constraints. It won't replace neither an ASN.1 parser nor an ASN.1 compiler. Actually, it has been written to be able to encode and decode broken ASN.1. It can handle corrupted encoded strings and can also create those.
-
-ASN.1 engine
-^^^^^^^^^^^^
-
-Note: many of the classes definitions presented here use metaclasses. If you don't look precisely at the source code and you only rely on my captures, you may think they sometimes exhibit a kind of magic behavior.
-``
-Scapy ASN.1 engine provides classes to link objects and their tags. They inherit from the ``ASN1_Class``. The first one is ``ASN1_Class_UNIVERSAL``, which provide tags for most Universal objects. Each new context (``SNMP``, ``X509``) will inherit from it and add its own objects.
-
-::
-
- class ASN1_Class_UNIVERSAL(ASN1_Class):
- name = "UNIVERSAL"
- # [...]
- BOOLEAN = 1
- INTEGER = 2
- BIT_STRING = 3
- # [...]
-
- class ASN1_Class_SNMP(ASN1_Class_UNIVERSAL):
- name="SNMP"
- PDU_GET = 0xa0
- PDU_NEXT = 0xa1
- PDU_RESPONSE = 0xa2
-
- class ASN1_Class_X509(ASN1_Class_UNIVERSAL):
- name="X509"
- CONT0 = 0xa0
- CONT1 = 0xa1
- # [...]
-
-All ASN.1 objects are represented by simple Python instances that act as nutshells for the raw values. The simple logic is handled by ``ASN1_Object`` whose they inherit from. Hence they are quite simple::
-
- class ASN1_INTEGER(ASN1_Object):
- tag = ASN1_Class_UNIVERSAL.INTEGER
-
- class ASN1_STRING(ASN1_Object):
- tag = ASN1_Class_UNIVERSAL.STRING
-
- class ASN1_BIT_STRING(ASN1_STRING):
- tag = ASN1_Class_UNIVERSAL.BIT_STRING
-
-These instances can be assembled to create an ASN.1 tree::
-
- >>> x=ASN1_SEQUENCE([ASN1_INTEGER(7),ASN1_STRING("egg"),ASN1_SEQUENCE([ASN1_BOOLEAN(False)])])
- >>> x
- , , ]]>]]>
- >>> x.show()
- # ASN1_SEQUENCE:
-
-
- # ASN1_SEQUENCE:
-
-
-Encoding engines
-^^^^^^^^^^^^^^^^^
-
-As with the standard, ASN.1 and encoding are independent. We have just seen how to create a compounded ASN.1 object. To encode or decode it, we need to choose an encoding rule. Scapy provides only BER for the moment (actually, it may be DER. DER looks like BER except only minimal encoding is authorised which may well be what I did). I call this an ASN.1 codec.
-
-Encoding and decoding are done using class methods provided by the codec. For example the ``BERcodec_INTEGER`` class provides a ``.enc()`` and a ``.dec()`` class methods that can convert between an encoded string and a value of their type. They all inherit from BERcodec_Object which is able to decode objects from any type::
-
- >>> BERcodec_INTEGER.enc(7)
- '\x02\x01\x07'
- >>> BERcodec_BIT_STRING.enc("egg")
- '\x03\x03egg'
- >>> BERcodec_STRING.enc("egg")
- '\x04\x03egg'
- >>> BERcodec_STRING.dec('\x04\x03egg')
- (, '')
- >>> BERcodec_STRING.dec('\x03\x03egg')
- Traceback (most recent call last):
- File "", line 1, in ?
- File "/usr/bin/scapy", line 2099, in dec
- return cls.do_dec(s, context, safe)
- File "/usr/bin/scapy", line 2178, in do_dec
- l,s,t = cls.check_type_check_len(s)
- File "/usr/bin/scapy", line 2076, in check_type_check_len
- l,s3 = cls.check_type_get_len(s)
- File "/usr/bin/scapy", line 2069, in check_type_get_len
- s2 = cls.check_type(s)
- File "/usr/bin/scapy", line 2065, in check_type
- (cls.__name__, ord(s[0]), ord(s[0]),cls.tag), remaining=s)
- BER_BadTag_Decoding_Error: BERcodec_STRING: Got tag [3/0x3] while expecting
- ### Already decoded ###
- None
- ### Remaining ###
- '\x03\x03egg'
- >>> BERcodec_Object.dec('\x03\x03egg')
- (, '')
-
-ASN.1 objects are encoded using their ``.enc()`` method. This method must be called with the codec we want to use. All codecs are referenced in the ASN1_Codecs object. ``raw()`` can also be used. In this case, the default codec (``conf.ASN1_default_codec``) will be used.
-
-::
-
- >>> x.enc(ASN1_Codecs.BER)
- '0\r\x02\x01\x07\x04\x03egg0\x03\x01\x01\x00'
- >>> raw(x)
- '0\r\x02\x01\x07\x04\x03egg0\x03\x01\x01\x00'
- >>> xx,remain = BERcodec_Object.dec(_)
- >>> xx.show()
- # ASN1_SEQUENCE:
-
-
- # ASN1_SEQUENCE:
-
-
- >>> remain
- ''
-
-By default, decoding is done using the ``Universal`` class, which means objects defined in the ``Context`` class will not be decoded. There is a good reason for that: the decoding depends on the context!
-
-::
-
- >>> cert="""
- ... MIIF5jCCA86gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMC
- ... VVMxHTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNB
- ... bWVyaWNhIE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIg
- ... Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyOTA2MDAw
- ... MFoXDTM3MDkyODIzNDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRB
- ... T0wgVGltZSBXYXJuZXIgSW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUg
- ... SW5jLjE3MDUGA1UEAxMuQU9MIFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNh
- ... dGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
- ... ggIBALQ3WggWmRToVbEbJGv8x4vmh6mJ7ouZzU9AhqS2TcnZsdw8TQ2FTBVs
- ... RotSeJ/4I/1n9SQ6aF3Q92RhQVSji6UI0ilbm2BPJoPRYxJWSXakFsKlnUWs
- ... i4SVqBax7J/qJBrvuVdcmiQhLE0OcR+mrF1FdAOYxFSMFkpBd4aVdQxHAWZg
- ... /BXxD+r1FHjHDtdugRxev17nOirYlxcwfACtCJ0zr7iZYYCLqJV+FNwSbKTQ
- ... 2O9ASQI2+W6p1h2WVgSysy0WVoaP2SBXgM1nEG2wTPDaRrbqJS5Gr42whTg0
- ... ixQmgiusrpkLjhTXUr2eacOGAgvqdnUxCc4zGSGFQ+aJLZ8lN2fxI2rSAG2X
- ... +Z/nKcrdH9cG6rjJuQkhn8g/BsXS6RJGAE57COtCPStIbp1n3UsC5ETzkxml
- ... J85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86QNZ8MUhy/XCFh
- ... EVsVS6kkUfykXPcXnbDS+gfpj1bkGoxoigTTfFrjnqKhynFbotSg5ymFXQNo
- ... Kk/SBtc9+cMDLz9l+WceR0DTYw/j1Y75hauXTLPXJuuWCpTehTacyH+BCQJJ
- ... Kg71ZDIMgtG6aoIbs0t0EfOMd9afv9w3pKdVBC/UMejTRrkDfNoSTllkt1Ex
- ... MVCgyhwn2RAurda9EGYrw7AiShJbAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMB
- ... Af8wHQYDVR0OBBYEFE9pbQN+nZ8HGEO8txBO1b+pxCAoMB8GA1UdIwQYMBaA
- ... FE9pbQN+nZ8HGEO8txBO1b+pxCAoMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG
- ... 9w0BAQUFAAOCAgEAO/Ouyuguh4X7ZVnnrREUpVe8WJ8kEle7+z802u6teio0
- ... cnAxa8cZmIDJgt43d15Ui47y6mdPyXSEkVYJ1eV6moG2gcKtNuTxVBFT8zRF
- ... ASbI5Rq8NEQh3q0l/HYWdyGQgJhXnU7q7C+qPBR7V8F+GBRn7iTGvboVsNIY
- ... vbdVgaxTwOjdaRITQrcCtQVBynlQboIOcXKTRuidDV29rs4prWPVVRaAMCf/
- ... drr3uNZK49m1+VLQTkCpx+XCMseqdiThawVQ68W/ClTluUI8JPu3B5wwn3la
- ... 5uBAUhX0/Kr0VvlEl4ftDmVyXr4m+02kLQgH3thcoNyBM5kYJRF3p+v9WAks
- ... mWsbivNSPxpNSGDxoPYzAlOL7SUJuA0t7Zdz7NeWH45gDtoQmy8YJPamTQr5
- ... O8t1wswvziRpyQoijlmn94IM19drNZxDAGrElWe6nEXLuA4399xOAU++CrYD
- ... 062KRffaJ00psUjf5BHklka9bAI+1lHIlRcBFanyqqryvy9lG2/QuRqT9Y41
- ... xICHPpQvZuTpqP9BnHAqTyo5GJUefvthATxRCC4oGKQWDzH9OmwjkyB24f0H
- ... hdFbP9IcczLd+rn4jM8Ch3qaluTtT4mNU0OrDhPAARW0eTjb/G49nlG2uBOL
- ... Z8/5fNkiHfZdxRwBL5joeiQYvITX+txyW/fBOmg=
- ... """.decode("base64")
- >>> (dcert,remain) = BERcodec_Object.dec(cert)
- Traceback (most recent call last):
- File "", line 1, in ?
- File "/usr/bin/scapy", line 2099, in dec
- return cls.do_dec(s, context, safe)
- File "/usr/bin/scapy", line 2094, in do_dec
- return codec.dec(s,context,safe)
- File "/usr/bin/scapy", line 2099, in dec
- return cls.do_dec(s, context, safe)
- File "/usr/bin/scapy", line 2218, in do_dec
- o,s = BERcodec_Object.dec(s, context, safe)
- File "/usr/bin/scapy", line 2099, in dec
- return cls.do_dec(s, context, safe)
- File "/usr/bin/scapy", line 2094, in do_dec
- return codec.dec(s,context,safe)
- File "/usr/bin/scapy", line 2099, in dec
- return cls.do_dec(s, context, safe)
- File "/usr/bin/scapy", line 2218, in do_dec
- o,s = BERcodec_Object.dec(s, context, safe)
- File "/usr/bin/scapy", line 2099, in dec
- return cls.do_dec(s, context, safe)
- File "/usr/bin/scapy", line 2092, in do_dec
- raise BER_Decoding_Error("Unknown prefix [%02x] for [%r]" % (p,t), remaining=s)
- BER_Decoding_Error: Unknown prefix [a0] for ['\xa0\x03\x02\x01\x02\x02\x01\x010\r\x06\t*\x86H...']
- ### Already decoded ###
- [[]]
- ### Remaining ###
- '\xa0\x03\x02\x01\x02\x02\x01\x010\r\x06\t*\x86H\x86\xf7\r\x01\x01\x05\x05\x000\x81\x831\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x1d0\x1b\x06\x03U\x04\n\x13\x14AOL Time Warner Inc.1\x1c0\x1a\x06\x03U\x04\x0b\x13\x13America Online Inc.1705\x06\x03U\x04\x03\x13.AOL Time Warner Root Certification Authority 20\x1e\x17\r020529060000Z\x17\r370928234300Z0\x81\x831\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x1d0\x1b\x06\x03U\x04\n\x13\x14AOL Time Warner Inc.1\x1c0\x1a\x06\x03U\x04\x0b\x13\x13America Online Inc.1705\x06\x03U\x04\x03\x13.AOL Time Warner Root Certification Authority 20\x82\x02"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x02\x0f\x000\x82\x02\n\x02\x82\x02\x01\x00\xb47Z\x08\x16\x99\x14\xe8U\xb1\x1b$k\xfc\xc7\x8b\xe6\x87\xa9\x89\xee\x8b\x99\xcdO@\x86\xa4\xb6M\xc9\xd9\xb1\xdc\xd6Q\xc8\x95\x17\x01\x15\xa9\xf2\xaa\xaa\xf2\xbf/e\x1bo\xd0\xb9\x1a\x93\xf5\x8e5\xc4\x80\x87>\x94/f\xe4\xe9\xa8\xffA\x9cp*O*9\x18\x95\x1e~\xfba\x01>> (dcert,remain) = BERcodec_Object.dec(cert, context=ASN1_Class_X509)
- >>> dcert.show()
- # ASN1_SEQUENCE:
- # ASN1_SEQUENCE:
- # ASN1_X509_CONT0:
-
-
- # ASN1_SEQUENCE:
-
-
- # ASN1_SEQUENCE:
- # ASN1_SET:
- # ASN1_SEQUENCE:
-
-
- # ASN1_SET:
- # ASN1_SEQUENCE:
-
-
- # ASN1_SET:
- # ASN1_SEQUENCE:
-
-
- # ASN1_SET:
- # ASN1_SEQUENCE:
-
-
- # ASN1_SEQUENCE:
-
-
- # ASN1_SEQUENCE:
- # ASN1_SET:
- # ASN1_SEQUENCE:
-
-
- # ASN1_SET:
- # ASN1_SEQUENCE:
-
-
- # ASN1_SET:
- # ASN1_SEQUENCE:
-
-
- # ASN1_SET:
- # ASN1_SEQUENCE:
-
-
- # ASN1_SEQUENCE:
- # ASN1_SEQUENCE:
-
-
-
- # ASN1_X509_CONT3:
- # ASN1_SEQUENCE:
- # ASN1_SEQUENCE:
-
-
-
- # ASN1_SEQUENCE:
-
-
- # ASN1_SEQUENCE:
-
-
- # ASN1_SEQUENCE:
-
-
-
- # ASN1_SEQUENCE:
-
-
- \xd6Q\xc8\x95\x17\x01\x15\xa9\xf2\xaa\xaa\xf2\xbf/e\x1bo\xd0\xb9\x1a\x93\xf5\x8e5\xc4\x80\x87>\x94/f\xe4\xe9\xa8\xffA\x9cp*O*9\x18\x95\x1e~\xfba\x01
-
-ASN.1 layers
-^^^^^^^^^^^^
-
-While this may be nice, it's only an ASN.1 encoder/decoder. Nothing related to Scapy yet.
-
-ASN.1 fields
-~~~~~~~~~~~~
-
-Scapy provides ASN.1 fields. They will wrap ASN.1 objects and provide the necessary logic to bind a field name to the value. ASN.1 packets will be described as a tree of ASN.1 fields. Then each field name will be made available as a normal ``Packet`` object, in a flat flavor (ex: to access the version field of a SNMP packet, you don't need to know how many containers wrap it).
-
-Each ASN.1 field is linked to an ASN.1 object through its tag.
-
-
-ASN.1 packets
-~~~~~~~~~~~~~
-
-ASN.1 packets inherit from the Packet class. Instead of a ``fields_desc`` list of fields, they define ``ASN1_codec`` and ``ASN1_root`` attributes. The first one is a codec (for example: ``ASN1_Codecs.BER``), the second one is a tree compounded with ASN.1 fields.
-
-A complete example: SNMP
-------------------------
-
-SNMP defines new ASN.1 objects. We need to define them::
-
- class ASN1_Class_SNMP(ASN1_Class_UNIVERSAL):
- name="SNMP"
- PDU_GET = 0xa0
- PDU_NEXT = 0xa1
- PDU_RESPONSE = 0xa2
- PDU_SET = 0xa3
- PDU_TRAPv1 = 0xa4
- PDU_BULK = 0xa5
- PDU_INFORM = 0xa6
- PDU_TRAPv2 = 0xa7
-
-These objects are PDU, and are in fact new names for a sequence container (this is generally the case for context objects: they are old containers with new names). This means creating the corresponding ASN.1 objects and BER codecs is simplistic::
-
- class ASN1_SNMP_PDU_GET(ASN1_SEQUENCE):
- tag = ASN1_Class_SNMP.PDU_GET
-
- class ASN1_SNMP_PDU_NEXT(ASN1_SEQUENCE):
- tag = ASN1_Class_SNMP.PDU_NEXT
-
- # [...]
-
- class BERcodec_SNMP_PDU_GET(BERcodec_SEQUENCE):
- tag = ASN1_Class_SNMP.PDU_GET
-
- class BERcodec_SNMP_PDU_NEXT(BERcodec_SEQUENCE):
- tag = ASN1_Class_SNMP.PDU_NEXT
-
- # [...]
-
-Metaclasses provide the magic behind the fact that everything is automatically registered and that ASN.1 objects and BER codecs can find each other.
-
-The ASN.1 fields are also trivial::
-
- class ASN1F_SNMP_PDU_GET(ASN1F_SEQUENCE):
- ASN1_tag = ASN1_Class_SNMP.PDU_GET
-
- class ASN1F_SNMP_PDU_NEXT(ASN1F_SEQUENCE):
- ASN1_tag = ASN1_Class_SNMP.PDU_NEXT
-
- # [...]
-
-Now, the hard part, the ASN.1 packet::
-
- SNMP_error = { 0: "no_error",
- 1: "too_big",
- # [...]
- }
-
- SNMP_trap_types = { 0: "cold_start",
- 1: "warm_start",
- # [...]
- }
-
- class SNMPvarbind(ASN1_Packet):
- ASN1_codec = ASN1_Codecs.BER
- ASN1_root = ASN1F_SEQUENCE( ASN1F_OID("oid","1.3"),
- ASN1F_field("value",ASN1_NULL(0))
- )
-
-
- class SNMPget(ASN1_Packet):
- ASN1_codec = ASN1_Codecs.BER
- ASN1_root = ASN1F_SNMP_PDU_GET( ASN1F_INTEGER("id",0),
- ASN1F_enum_INTEGER("error",0, SNMP_error),
- ASN1F_INTEGER("error_index",0),
- ASN1F_SEQUENCE_OF("varbindlist", [], SNMPvarbind)
- )
-
- class SNMPnext(ASN1_Packet):
- ASN1_codec = ASN1_Codecs.BER
- ASN1_root = ASN1F_SNMP_PDU_NEXT( ASN1F_INTEGER("id",0),
- ASN1F_enum_INTEGER("error",0, SNMP_error),
- ASN1F_INTEGER("error_index",0),
- ASN1F_SEQUENCE_OF("varbindlist", [], SNMPvarbind)
- )
- # [...]
-
- class SNMP(ASN1_Packet):
- ASN1_codec = ASN1_Codecs.BER
- ASN1_root = ASN1F_SEQUENCE(
- ASN1F_enum_INTEGER("version", 1, {0:"v1", 1:"v2c", 2:"v2", 3:"v3"}),
- ASN1F_STRING("community","public"),
- ASN1F_CHOICE("PDU", SNMPget(),
- SNMPget, SNMPnext, SNMPresponse, SNMPset,
- SNMPtrapv1, SNMPbulk, SNMPinform, SNMPtrapv2)
- )
- def answers(self, other):
- return ( isinstance(self.PDU, SNMPresponse) and
- ( isinstance(other.PDU, SNMPget) or
- isinstance(other.PDU, SNMPnext) or
- isinstance(other.PDU, SNMPset) ) and
- self.PDU.id == other.PDU.id )
- # [...]
- bind_layers( UDP, SNMP, sport=161)
- bind_layers( UDP, SNMP, dport=161)
-
-That wasn't that much difficult. If you think that can't be that short to implement SNMP encoding/decoding and that I may have cut too much, just look at the complete source code.
-
-Now, how to use it? As usual::
-
- >>> a=SNMP(version=3, PDU=SNMPget(varbindlist=[SNMPvarbind(oid="1.2.3",value=5),
- ... SNMPvarbind(oid="3.2.1",value="hello")]))
- >>> a.show()
- ###[ SNMP ]###
- version= v3
- community= 'public'
- \PDU\
- |###[ SNMPget ]###
- | id= 0
- | error= no_error
- | error_index= 0
- | \varbindlist\
- | |###[ SNMPvarbind ]###
- | | oid= '1.2.3'
- | | value= 5
- | |###[ SNMPvarbind ]###
- | | oid= '3.2.1'
- | | value= 'hello'
- >>> hexdump(a)
- 0000 30 2E 02 01 03 04 06 70 75 62 6C 69 63 A0 21 02 0......public.!.
- 0010 01 00 02 01 00 02 01 00 30 16 30 07 06 02 2A 03 ........0.0...*.
- 0020 02 01 05 30 0B 06 02 7A 01 04 05 68 65 6C 6C 6F ...0...z...hello
- >>> send(IP(dst="1.2.3.4")/UDP()/SNMP())
- .
- Sent 1 packets.
- >>> SNMP(raw(a)).show()
- ###[ SNMP ]###
- version=
- community=
- \PDU\
- |###[ SNMPget ]###
- | id=
- | error=
- | error_index=
- | \varbindlist\
- | |###[ SNMPvarbind ]###
- | | oid=
- | | value=
- | |###[ SNMPvarbind ]###
- | | oid=
- | | value=
-
-
-
-Resolving OID from a MIB
-------------------------
-
-About OID objects
-^^^^^^^^^^^^^^^^^
-
-OID objects are created with an ``ASN1_OID`` class::
-
- >>> o1=ASN1_OID("2.5.29.10")
- >>> o2=ASN1_OID("1.2.840.113549.1.1.1")
- >>> o1,o2
- (, )
-
-Loading a MIB
-^^^^^^^^^^^^^
-
-Scapy can parse MIB files and become aware of a mapping between an OID and its name::
-
- >>> load_mib("mib/*")
- >>> o1,o2
- (, )
-
-The MIB files I've used are attached to this page.
-
-Scapy's MIB database
-^^^^^^^^^^^^^^^^^^^^
-
-All MIB information is stored into the conf.mib object. This object can be used to find the OID of a name
-
-::
-
- >>> conf.mib.sha1_with_rsa_signature
- '1.2.840.113549.1.1.5'
-
-or to resolve an OID::
-
- >>> conf.mib._oidname("1.2.3.6.1.4.1.5")
- 'enterprises.5'
-
-It is even possible to graph it::
-
- >>> conf.mib._make_graph()
-
-
-
-Automata
-========
-
-Scapy enables to create easily network automata. Scapy does not stick to a specific model like Moore or Mealy automata. It provides a flexible way for you to choose your way to go.
-
-An automaton in Scapy is deterministic. It has different states. A start state and some end and error states. There are transitions from one state to another. Transitions can be transitions on a specific condition, transitions on the reception of a specific packet or transitions on a timeout. When a transition is taken, one or more actions can be run. An action can be bound to many transitions. Parameters can be passed from states to transitions, and from transitions to states and actions.
-
-From a programmer's point of view, states, transitions and actions are methods from an Automaton subclass. They are decorated to provide meta-information needed in order for the automaton to work.
-
-First example
--------------
-
-Let's begin with a simple example. I take the convention to write states with capitals, but anything valid with Python syntax would work as well.
-
-::
-
- class HelloWorld(Automaton):
- @ATMT.state(initial=1)
- def BEGIN(self):
- print("State=BEGIN")
-
- @ATMT.condition(BEGIN)
- def wait_for_nothing(self):
- print("Wait for nothing...")
- raise self.END()
-
- @ATMT.action(wait_for_nothing)
- def on_nothing(self):
- print("Action on 'nothing' condition")
-
- @ATMT.state(final=1)
- def END(self):
- print("State=END")
-
-In this example, we can see 3 decorators:
-
-* ``ATMT.state`` that is used to indicate that a method is a state, and that can
- have initial, final, stop and error optional arguments set to non-zero for special states.
-* ``ATMT.condition`` that indicate a method to be run when the automaton state
- reaches the indicated state. The argument is the name of the method representing that state
-* ``ATMT.action`` binds a method to a transition and is run when the transition is taken.
-
-Running this example gives the following result::
-
- >>> a=HelloWorld()
- >>> a.run()
- State=BEGIN
- Wait for nothing...
- Action on 'nothing' condition
- State=END
- >>> a.destroy()
-
-This simple automaton can be described with the following graph:
-
-.. image:: graphics/ATMT_HelloWorld.*
-
-The graph can be automatically drawn from the code with::
-
- >>> HelloWorld.graph()
-
-.. note:: An ``Automaton`` can be reset using ``restart()``. It is then possible to run it again.
-
-.. warning:: Remember to call ``destroy()`` once you're done using an Automaton. (especially on PyPy)
-
-Changing states
----------------
-
-The ``ATMT.state`` decorator transforms a method into a function that returns an exception. If you raise that exception, the automaton state will be changed. If the change occurs in a transition, actions bound to this transition will be called. The parameters given to the function replacing the method will be kept and finally delivered to the method. The exception has a method action_parameters that can be called before it is raised so that it will store parameters to be delivered to all actions bound to the current transition.
-
-As an example, let's consider the following state::
-
- @ATMT.state()
- def MY_STATE(self, param1, param2):
- print("state=MY_STATE. param1=%r param2=%r" % (param1, param2))
-
-This state will be reached with the following code::
-
- @ATMT.receive_condition(ANOTHER_STATE)
- def received_ICMP(self, pkt):
- if ICMP in pkt:
- raise self.MY_STATE("got icmp", pkt[ICMP].type)
-
-Let's suppose we want to bind an action to this transition, that will also need some parameters::
-
- @ATMT.action(received_ICMP)
- def on_ICMP(self, icmp_type, icmp_code):
- self.retaliate(icmp_type, icmp_code)
-
-The condition should become::
-
- @ATMT.receive_condition(ANOTHER_STATE)
- def received_ICMP(self, pkt):
- if ICMP in pkt:
- raise self.MY_STATE("got icmp", pkt[ICMP].type).action_parameters(pkt[ICMP].type, pkt[ICMP].code)
-
-Real example
-------------
-
-Here is a real example take from Scapy. It implements a TFTP client that can issue read requests.
-
-.. image:: graphics/ATMT_TFTP_read.*
-
-::
-
- class TFTP_read(Automaton):
- def parse_args(self, filename, server, sport = None, port=69, **kargs):
- Automaton.parse_args(self, **kargs)
- self.filename = filename
- self.server = server
- self.port = port
- self.sport = sport
-
- def master_filter(self, pkt):
- return ( IP in pkt and pkt[IP].src == self.server and UDP in pkt
- and pkt[UDP].dport == self.my_tid
- and (self.server_tid is None or pkt[UDP].sport == self.server_tid) )
-
- # BEGIN
- @ATMT.state(initial=1)
- def BEGIN(self):
- self.blocksize=512
- self.my_tid = self.sport or RandShort()._fix()
- bind_bottom_up(UDP, TFTP, dport=self.my_tid)
- self.server_tid = None
- self.res = b""
-
- self.l3 = IP(dst=self.server)/UDP(sport=self.my_tid, dport=self.port)/TFTP()
- self.last_packet = self.l3/TFTP_RRQ(filename=self.filename, mode="octet")
- self.send(self.last_packet)
- self.awaiting=1
-
- raise self.WAITING()
-
- # WAITING
- @ATMT.state()
- def WAITING(self):
- pass
-
- @ATMT.receive_condition(WAITING)
- def receive_data(self, pkt):
- if TFTP_DATA in pkt and pkt[TFTP_DATA].block == self.awaiting:
- if self.server_tid is None:
- self.server_tid = pkt[UDP].sport
- self.l3[UDP].dport = self.server_tid
- raise self.RECEIVING(pkt)
- @ATMT.action(receive_data)
- def send_ack(self):
- self.last_packet = self.l3 / TFTP_ACK(block = self.awaiting)
- self.send(self.last_packet)
-
- @ATMT.receive_condition(WAITING, prio=1)
- def receive_error(self, pkt):
- if TFTP_ERROR in pkt:
- raise self.ERROR(pkt)
-
- @ATMT.timeout(WAITING, 3)
- def timeout_waiting(self):
- raise self.WAITING()
- @ATMT.action(timeout_waiting)
- def retransmit_last_packet(self):
- self.send(self.last_packet)
-
- # RECEIVED
- @ATMT.state()
- def RECEIVING(self, pkt):
- recvd = pkt[Raw].load
- self.res += recvd
- self.awaiting += 1
- if len(recvd) == self.blocksize:
- raise self.WAITING()
- raise self.END()
-
- # ERROR
- @ATMT.state(error=1)
- def ERROR(self,pkt):
- split_bottom_up(UDP, TFTP, dport=self.my_tid)
- return pkt[TFTP_ERROR].summary()
-
- #END
- @ATMT.state(final=1)
- def END(self):
- split_bottom_up(UDP, TFTP, dport=self.my_tid)
- return self.res
-
-It can be run like this, for instance::
-
- >>> atmt = TFTP_read("my_file", "192.168.1.128")
- >>> atmt.run()
- >>> atmt.destroy()
-
-Detailed documentation
-----------------------
-
-Decorators
-^^^^^^^^^^
-Decorator for states
-~~~~~~~~~~~~~~~~~~~~
-
-States are methods decorated by the result of the ``ATMT.state`` function. It can take 4 optional parameters, ``initial``, ``final``, ``stop`` and ``error``, that, when set to ``True``, indicating that the state is an initial, final, stop or error state.
-
-.. note:: The ``initial`` state is called while starting the automata. The ``final`` step will tell the automata has reached its end. If you call ``atmt.stop()``, the automata will move to the ``stop`` step whatever its current state is. The ``error`` state will mark the automata as errored. If no ``stop`` state is specified, calling ``stop`` and ``forcestop`` will be equivalent.
-
-::
-
- class Example(Automaton):
- @ATMT.state(initial=1)
- def BEGIN(self):
- pass
-
- @ATMT.state()
- def SOME_STATE(self):
- pass
-
- @ATMT.state(final=1)
- def END(self):
- return "Result of the automaton: 42"
-
- @ATMT.state(stop=1)
- def STOP(self):
- print("SHUTTING DOWN...")
- # e.g. close sockets...
-
- @ATMT.condition(STOP)
- def is_stopping(self):
- raise self.END()
-
- @ATMT.state(error=1)
- def ERROR(self):
- return "Partial result, or explanation"
- # [...]
-
-Take for instance the TCP client:
-
-.. image:: graphics/ATMT_TCP_client.svg
-
-The ``START`` event is ``initial=1``, the ``STOP`` event is ``stop=1`` and the ``CLOSED`` event is ``final=1``.
-
-Decorators for transitions
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-Transitions are methods decorated by the result of one of ``ATMT.condition``, ``ATMT.receive_condition``, ``ATMT.eof``, ``ATMT.timeout``, ``ATMT.timer``. They all take as argument the state method they are related to. ``ATMT.timeout`` and ``ATMT.timer`` also have a mandatory ``timeout`` parameter to provide the timeout value in seconds. The difference between ``ATMT.timeout`` and ``ATMT.timer`` is that ``ATMT.timeout`` gets triggered only once. ``ATMT.timer`` get reloaded automatically, which is useful for sending keep-alive packets. ``ATMT.condition`` and ``ATMT.receive_condition`` have an optional ``prio`` parameter so that the order in which conditions are evaluated can be forced. The default priority is 0. Transitions with the same priority level are called in an undetermined order.
-
-When the automaton switches to a given state, the state's method is executed. Then transitions methods are called at specific moments until one triggers a new state (something like ``raise self.MY_NEW_STATE()``). First, right after the state's method returns, the ``ATMT.condition`` decorated methods are run by growing prio. Then each time a packet is received and accepted by the master filter all ``ATMT.receive_condition`` decorated hods are called by growing prio. When a timeout is reached since the time we entered into the current space, the corresponding ``ATMT.timeout`` decorated method is called. If the socket raises an ``EOFError`` (closed) during a state, the ``ATMT.EOF`` transition is called. Otherwise it raises an exception and the automaton exits.
-
-::
-
- class Example(Automaton):
- @ATMT.state()
- def WAITING(self):
- pass
-
- @ATMT.condition(WAITING)
- def it_is_raining(self):
- if not self.have_umbrella:
- raise self.ERROR_WET()
-
- @ATMT.receive_condition(WAITING, prio=1)
- def it_is_ICMP(self, pkt):
- if ICMP in pkt:
- raise self.RECEIVED_ICMP(pkt)
-
- @ATMT.receive_condition(WAITING, prio=2)
- def it_is_IP(self, pkt):
- if IP in pkt:
- raise self.RECEIVED_IP(pkt)
-
- @ATMT.timeout(WAITING, 10.0)
- def waiting_timeout(self):
- raise self.ERROR_TIMEOUT()
-
-Decorator for actions
-~~~~~~~~~~~~~~~~~~~~~
-
-Actions are methods that are decorated by the return of ``ATMT.action`` function. This function takes the transition method it is bound to as first parameter and an optional priority ``prio`` as a second parameter. The default priority is 0. An action method can be decorated many times to be bound to many transitions.
-
-::
-
- from random import random
-
- class Example(Automaton):
- @ATMT.state(initial=1)
- def BEGIN(self):
- pass
-
- @ATMT.state(final=1)
- def END(self):
- pass
-
- @ATMT.condition(BEGIN, prio=1)
- def maybe_go_to_end(self):
- if random() > 0.5:
- raise self.END()
-
- @ATMT.condition(BEGIN, prio=2)
- def certainly_go_to_end(self):
- raise self.END()
-
- @ATMT.action(maybe_go_to_end)
- def maybe_action(self):
- print("We are lucky...")
-
- @ATMT.action(certainly_go_to_end)
- def certainly_action(self):
- print("We are not lucky...")
-
- @ATMT.action(maybe_go_to_end, prio=1)
- @ATMT.action(certainly_go_to_end, prio=1)
- def always_action(self):
- print("This wasn't luck!...")
-
-The two possible outputs are::
-
- >>> a=Example()
- >>> a.run()
- We are not lucky...
- This wasn't luck!...
- >>> a.run()
- We are lucky...
- This wasn't luck!...
- >>> a.destroy()
-
-
-.. note:: If you want to pass a parameter to an action, you can use the ``action_parameters`` function while raising the next state.
-
-In the following example, the ``send_copy`` action takes a parameter passed by ``is_fin``::
-
- class Example(Automaton):
- @ATMT.state()
- def WAITING(self):
- pass
-
- @ATMT.state()
- def FIN_RECEIVED(self):
- pass
-
- @ATMT.receive_condition(WAITING)
- def is_fin(self, pkt):
- if pkt[TCP].flags.F:
- raise self.FIN_RECEIVED().action_parameters(pkt)
-
- @ATMT.action(is_fin)
- def send_copy(self, pkt):
- send(pkt)
-
-
-Methods to overload
-^^^^^^^^^^^^^^^^^^^
-
-Two methods are hooks to be overloaded:
-
-* The ``parse_args()`` method is called with arguments given at ``__init__()`` and ``run()``. Use that to parametrize the behavior of your automaton.
-
-* The ``master_filter()`` method is called each time a packet is sniffed and decides if it is interesting for the automaton. When working on a specific protocol, this is where you will ensure the packet belongs to the connection you are being part of, so that you do not need to make all the sanity checks in each transition.
-
-Timer configuration
-^^^^^^^^^^^^^^^^^^^
-
-Some protocols allow timer configuration. In order to configure timeout values during class initialization one may use ``timer_by_name()`` method, which returns ``Timer`` object associated with the given function name::
-
- class Example(Automaton):
- def __init__(self, *args, **kwargs):
- super(Example, self).__init__(*args, **kwargs)
- timer = self.timer_by_name("waiting_timeout")
- timer.set(1)
-
- @ATMT.state(initial=1)
- def WAITING(self):
- pass
-
- @ATMT.state(final=1)
- def END(self):
- pass
-
- @ATMT.timeout(WAITING, 10.0)
- def waiting_timeout(self):
- raise self.END()
-
-.. _pipetools:
-
-PipeTools
-=========
-
-Scapy's ``pipetool`` is a smart piping system allowing to perform complex stream data management.
-
-The goal is to create a sequence of steps with one or several inputs and one or several outputs, with a bunch of blocks in between.
-PipeTools can handle varied sources of data (and outputs) such as user input, pcap input, sniffing, wireshark...
-A pipe system is implemented by manually linking all its parts. It is possible to dynamically add an element while running or set multiple drains for the same source.
-
-.. note:: Pipetool default objects are located inside ``scapy.pipetool``
-
-Demo: sniff, anonymize, send to Wireshark
------------------------------------------
-
-The following code will sniff packets on the default interface, anonymize the source and destination IP addresses and pipe it all into Wireshark. Useful when posting online examples, for instance.
-
-.. code-block:: python3
-
- source = SniffSource(iface=conf.iface)
- wire = WiresharkSink()
- def transf(pkt):
- if not pkt or IP not in pkt:
- return pkt
- pkt[IP].src = "1.1.1.1"
- pkt[IP].dst = "2.2.2.2"
- return pkt
-
- source > TransformDrain(transf) > wire
- p = PipeEngine(source)
- p.start()
- p.wait_and_stop()
-
-The engine is pretty straightforward:
-
-.. image:: graphics/pipetool_demo.svg
-
-Let's run it:
-
-.. image:: graphics/animations/pipetool_demo.gif
-
-Class Types
------------
-
-There are 3 different class of objects used for data management:
-
-- ``Sources``
-- ``Drains``
-- ``Sinks``
-
-They are executed and handled by a :class:`~scapy.pipetool.PipeEngine` object.
-
-When running, a pipetool engine waits for any available data from the Source, and send it in the Drains linked to it.
-The data then goes from Drains to Drains until it arrives in a Sink, the final state of this data.
-
-Let's see with a basic demo how to build a pipetool system.
-
-.. image:: graphics/pipetool_engine.png
-
-For instance, this engine was generated with this code:
-
-.. code:: pycon
-
- >>> s = CLIFeeder()
- >>> s2 = CLIHighFeeder()
- >>> d1 = Drain()
- >>> d2 = TransformDrain(lambda x: x[::-1])
- >>> si1 = ConsoleSink()
- >>> si2 = QueueSink()
- >>>
- >>> s > d1
- >>> d1 > si1
- >>> d1 > si2
- >>>
- >>> s2 >> d1
- >>> d1 >> d2
- >>> d2 >> si1
- >>>
- >>> p = PipeEngine()
- >>> p.add(s)
- >>> p.add(s2)
- >>> p.graph(target="> the_above_image.png")
-
-``start()`` is used to start the :class:`~scapy.pipetool.PipeEngine`:
-
-.. code:: pycon
-
- >>> p.start()
-
-Now, let's play with it by sending some input data
-
-.. code:: pycon
-
- >>> s.send("foo")
- >'foo'
- >>> s2.send("bar")
- >>'rab'
- >>> s.send("i like potato")
- >'i like potato'
- >>> print(si2.recv(), ":", si2.recv())
- foo : i like potato
-
-Let's study what happens here:
-
-- there are **two canals** in a :class:`~scapy.pipetool.PipeEngine`, a lower one and a higher one. Some Sources write on the lower one, some on the higher one and some on both.
-- most sources can be linked to any drain, on both lower and higher canals. The use of ``>`` indicates a link on the low canal, and ``>>`` on the higher one.
-- when we send some data in ``s``, which is on the lower canal, as shown above, it goes through the :class:`~scapy.pipetool.Drain` then is sent to the :class:`~.scapy.pipetool.QueueSink` and to the :class:`~scapy.pipetool.ConsoleSink`
-- when we send some data in ``s2``, it goes through the Drain, then the TransformDrain where the data is reversed (see the lambda), before being sent to :class:`~scapy.pipetool.ConsoleSink` only. This explains why we only have the data of the lower sources inside the QueueSink: the higher one has not been linked.
-
-Most of the sinks receive from both lower and upper canals. This is verifiable using the `help(ConsoleSink)`
-
-.. code:: pycon
-
- >>> help(ConsoleSink)
- Help on class ConsoleSink in module scapy.pipetool:
- class ConsoleSink(Sink)
- | Print messages on low and high entries
- | +-------+
- | >>-|--. |->>
- | | print |
- | >-|--' |->
- | +-------+
- |
- [...]
-
-
-Sources
-^^^^^^^
-
-A Source is a class that generates some data.
-
-There are several source types integrated with Scapy, usable as-is, but you may
-also create yours.
-
-Default Source classes
-~~~~~~~~~~~~~~~~~~~~~~
-
-For any of those class, have a look at ``help([theclass])`` to get more information or the required parameters.
-
-- :class:`~scapy.pipetool.CLIFeeder` : a source especially used in interactive software. its ``send(data)`` generates the event data on the lower canal
-- :class:`~scapy.pipetool.CLIHighFeeder` : same than CLIFeeder, but writes on the higher canal
-- :class:`~scapy.pipetool.PeriodicSource` : Generate messages periodically on the low canal.
-- :class:`~scapy.pipetool.AutoSource`: the default source, that must be extended to create custom sources.
-
-Create a custom Source
-~~~~~~~~~~~~~~~~~~~~~~
-
-To create a custom source, one must extend the :class:`~scapy.pipetool.AutoSource` class.
-
-.. note::
-
- Do NOT use the default :class:`~scapy.pipetool.Source` class except if you are really sure of what you are doing: it is only used internally, and is missing some implementation. The :class:`~scapy.pipetool.AutoSource` is made to be used.
-
-
-To send data through it, the object must call its ``self._gen_data(msg)`` or ``self._gen_high_data(msg)`` functions, which send the data into the PipeEngine.
-
-The Source should also (if possible), set ``self.is_exhausted`` to ``True`` when empty, to allow the clean stop of the :class:`~scapy.pipetool.PipeEngine`. If the source is infinite, it will need a force-stop (see PipeEngine below)
-
-For instance, here is how :class:`~scapy.pipetool.CLIHighFeeder` is implemented:
-
-.. code:: python3
-
- class CLIFeeder(CLIFeeder):
- def send(self, msg):
- self._gen_high_data(msg)
- def close(self):
- self.is_exhausted = True
-
-Drains
-^^^^^^
-
-Default Drain classes
-~~~~~~~~~~~~~~~~~~~~~
-
-Drains need to be linked on the entry that you are using. It can be either on the lower one (using ``>``) or the upper one (using ``>>``).
-See the basic example above.
-
-- :class:`~scapy.pipetool.Drain` : the most basic Drain possible. Will pass on both low and high entry if linked properly.
-- :class:`~scapy.pipetool.TransformDrain` : Apply a function to messages on low and high entry
-- :class:`~scapy.pipetool.UpDrain` : Repeat messages from low entry to high exit
-- :class:`~scapy.pipetool.DownDrain` : Repeat messages from high entry to low exit
-
-Create a custom Drain
-~~~~~~~~~~~~~~~~~~~~~
-
-To create a custom drain, one must extend the :class:`~scapy.pipetool.Drain` class.
-
-A :class:`~scapy.pipetool.Drain` object will receive data from the lower canal in its ``push`` method, and from the higher canal from its ``high_push`` method.
-
-To send the data back into the next linked Drain / Sink, it must call the ``self._send(msg)`` or ``self._high_send(msg)`` methods.
-
-For instance, here is how :class:`~scapy.pipetool.TransformDrain` is implemented::
-
- class TransformDrain(Drain):
- def __init__(self, f, name=None):
- Drain.__init__(self, name=name)
- self.f = f
- def push(self, msg):
- self._send(self.f(msg))
- def high_push(self, msg):
- self._high_send(self.f(msg))
-
-Sinks
-^^^^^
-
-Sinks are destinations for messages.
-
-A :py:class:`~scapy.pipetool.Sink` receives data like a :py:class:`~scapy.pipetool.Drain`, but doesn't send any
-messages after it.
-
-Messages on the low entry come from :py:meth:`~scapy.pipetool.Sink.push`, and messages on the
-high entry come from :py:meth:`~scapy.pipetool.Sink.high_push`.
-
-Default Sinks classes
-~~~~~~~~~~~~~~~~~~~~~
-
-- :class:`~scapy.pipetool.ConsoleSink` : Print messages on low and high entries to ``stdout``
-- :class:`~scapy.pipetool.RawConsoleSink` : Print messages on low and high entries, using os.write
-- :class:`~scapy.pipetool.TermSink` : Prints messages on the low and high entries, on a separate terminal
-- :class:`~scapy.pipetool.QueueSink` : Collects messages on the low and high entries into a :py:class:`Queue`
-
-Create a custom Sink
-~~~~~~~~~~~~~~~~~~~~
-
-To create a custom sink, one must extend :py:class:`~scapy.pipetool.Sink` and implement
-:py:meth:`~scapy.pipetool.Sink.push` and/or :py:meth:`~scapy.pipetool.Sink.high_push`.
-
-This is a simplified version of :py:class:`~scapy.pipetool.ConsoleSink`:
-
-.. code-block:: python3
-
- class ConsoleSink(Sink):
- def push(self, msg):
- print(">%r" % msg)
- def high_push(self, msg):
- print(">>%r" % msg)
-
-Link objects
-------------
-
-As shown in the example, most sources can be linked to any drain, on both low
-and high entry.
-
-The use of ``>`` indicates a link on the low entry, and ``>>`` on the high
-entry.
-
-For example, to link ``a``, ``b`` and ``c`` on the low entries:
-
-.. code-block:: pycon
-
- >>> a = CLIFeeder()
- >>> b = Drain()
- >>> c = ConsoleSink()
- >>> a > b > c
- >>> p = PipeEngine()
- >>> p.add(a)
-
-This wouldn't link the high entries, so something like this would do nothing:
-
-.. code-block:: pycon
-
- >>> a2 = CLIHighFeeder()
- >>> a2 >> b
- >>> a2.send("hello")
-
-Because ``b`` (:py:class:`~scapy.pipetool.Drain`) and ``c`` (:py:class:`scapy.pipetool.ConsoleSink`) are not
-linked on the high entry.
-
-However, using a :py:class:`~scapy.pipetool.DownDrain` would bring the high messages from
-:py:class:`~scapy.pipetool.CLIHighFeeder` to the lower channel:
-
-.. code-block:: pycon
-
- >>> a2 = CLIHighFeeder()
- >>> b2 = DownDrain()
- >>> a2 >> b2
- >>> b2 > b
- >>> a2.send("hello")
-
-The PipeEngine class
---------------------
-
-The :class:`~scapy.pipetool.PipeEngine` class is the core class of the Pipetool system. It must be initialized and passed the list of all Sources.
-
-There are two ways of passing sources:
-
-- during initialization: ``p = PipeEngine(source1, source2, ...)``
-- using the ``add(source)`` method
-
-A :class:`~scapy.pipetool.PipeEngine` class must be started with ``.start()`` function. It may be force-stopped with the ``.stop()``, or cleanly stopped with ``.wait_and_stop()``
-
-A clean stop only works if the Sources is exhausted (has no data to send left).
-
-It can be printed into a graph using ``.graph()`` methods. see ``help(do_graph)`` for the list of available keyword arguments.
-
-Scapy advanced PipeTool objects
--------------------------------
-
-.. note:: Unlike the previous objects, those are not located in ``scapy.pipetool`` but in ``scapy.scapypipes``
-
-Now that you know the default PipeTool objects, here are some more advanced ones, based on packet functionalities.
-
-- :class:`~scapy.scapypipes.SniffSource` : Read packets from an interface and send them to low exit.
-- :class:`~scapy.scapypipes.RdpcapSource` : Read packets from a PCAP file send them to low exit.
-- :class:`~scapy.scapypipes.InjectSink` : Packets received on low input are injected (sent) to an interface
-- :class:`~scapy.scapypipes.WrpcapSink` : Packets received on low input are written to PCAP file
-- :class:`~scapy.scapypipes.UDPDrain` : UDP payloads received on high entry are sent over UDP (complicated, have a look at ``help(UDPDrain)``)
-- :class:`~scapy.scapypipes.FDSourceSink` : Use a file descriptor as source and sink
-- :class:`~scapy.scapypipes.TCPConnectPipe`: TCP connect to addr:port and use it as source and sink
-- :class:`~scapy.scapypipes.TCPListenPipe` : TCP listen on [addr:]port and use the first connection as source and sink (complicated, have a look at ``help(TCPListenPipe)``)
-
-Triggering
-----------
-
-Some special sort of Drains exists: the Trigger Drains.
-
-Trigger Drains are special drains, that on receiving data not only pass it by but also send a "Trigger" input, that is received and handled by the next triggered drain (if it exists).
-
-For example, here is a basic :class:`~scapy.scapypipes.TriggerDrain` usage:
-
-.. code:: pycon
-
- >>> a = CLIFeeder()
- >>> d = TriggerDrain(lambda msg: True) # Pass messages and trigger when a condition is met
- >>> d2 = TriggeredValve()
- >>> s = ConsoleSink()
- >>> a > d > d2 > s
- >>> d ^ d2 # Link the triggers
- >>> p = PipeEngine(s)
- >>> p.start()
- INFO: Pipe engine thread started.
- >>>
- >>> a.send("this will be printed")
- >'this will be printed'
- >>> a.send("this won't, because the valve was switched")
- >>> a.send("this will, because the valve was switched again")
- >'this will, because the valve was switched again'
- >>> p.stop()
-
-Several triggering Drains exist, they are pretty explicit. It is highly recommended to check the doc using ``help([the class])``
-
-- :class:`~scapy.scapypipes.TriggeredMessage` : Send a preloaded message when triggered and trigger in chain
-- :class:`~scapy.scapypipes.TriggerDrain` : Pass messages and trigger when a condition is met
-- :class:`~scapy.scapypipes.TriggeredValve` : Let messages alternatively pass or not, changing on trigger
-- :class:`~scapy.scapypipes.TriggeredQueueingValve` : Let messages alternatively pass or queued, changing on trigger
-- :class:`~scapy.scapypipes.TriggeredSwitch` : Let messages alternatively high or low, changing on trigger
diff --git a/doc/scapy/advanced_usage/asn1_snmp.rst b/doc/scapy/advanced_usage/asn1_snmp.rst
new file mode 100644
index 00000000000..feccf1f855d
--- /dev/null
+++ b/doc/scapy/advanced_usage/asn1_snmp.rst
@@ -0,0 +1,485 @@
+ASN.1 and SNMP
+==============
+
+What is ASN.1?
+--------------
+
+.. note::
+
+ This is only my view on ASN.1, explained as simply as possible. For more theoretical or academic views, I'm sure you'll find better on the Internet.
+
+ASN.1 is a notation whose goal is to specify formats for data exchange. It is independent of the way data is encoded. Data encoding is specified in Encoding Rules.
+
+The most used encoding rules are BER (Basic Encoding Rules) and DER (Distinguished Encoding Rules). Both look the same, but the latter is specified to guarantee uniqueness of encoding. This property is quite interesting when speaking about cryptography, hashes, and signatures.
+
+ASN.1 provides basic objects: integers, many kinds of strings, floats, booleans, containers, etc. They are grouped in the so-called Universal class. A given protocol can provide other objects which will be grouped in the Context class. For example, SNMP defines PDU_GET or PDU_SET objects. There are also the Application and Private classes.
+
+Each of these objects is given a tag that will be used by the encoding rules. Tags from 1 are used for Universal class. 1 is boolean, 2 is an integer, 3 is a bit string, 6 is an OID, 48 is for a sequence. Tags from the ``Context`` class begin at 0xa0. When encountering an object tagged by 0xa0, we'll need to know the context to be able to decode it. For example, in SNMP context, 0xa0 is a PDU_GET object, while in X509 context, it is a container for the certificate version.
+
+Other objects are created by assembling all those basic brick objects. The composition is done using sequences and arrays (sets) of previously defined or existing objects. The final object (an X509 certificate, a SNMP packet) is a tree whose non-leaf nodes are sequences and sets objects (or derived context objects), and whose leaf nodes are integers, strings, OID, etc.
+
+Scapy and ASN.1
+---------------
+
+Scapy provides a way to easily encode or decode ASN.1 and also program those encoders/decoders. It is quite laxer than what an ASN.1 parser should be, and it kind of ignores constraints. It won't replace neither an ASN.1 parser nor an ASN.1 compiler. Actually, it has been written to be able to encode and decode broken ASN.1. It can handle corrupted encoded strings and can also create those.
+
+ASN.1 engine
+^^^^^^^^^^^^
+
+Note: many of the classes definitions presented here use metaclasses. If you don't look precisely at the source code and you only rely on my captures, you may think they sometimes exhibit a kind of magic behavior.
+``
+Scapy ASN.1 engine provides classes to link objects and their tags. They inherit from the ``ASN1_Class``. The first one is ``ASN1_Class_UNIVERSAL``, which provide tags for most Universal objects. Each new context (``SNMP``, ``X509``) will inherit from it and add its own objects.
+
+::
+
+ class ASN1_Class_UNIVERSAL(ASN1_Class):
+ name = "UNIVERSAL"
+ # [...]
+ BOOLEAN = 1
+ INTEGER = 2
+ BIT_STRING = 3
+ # [...]
+
+ class ASN1_Class_SNMP(ASN1_Class_UNIVERSAL):
+ name="SNMP"
+ PDU_GET = 0xa0
+ PDU_NEXT = 0xa1
+ PDU_RESPONSE = 0xa2
+
+ class ASN1_Class_X509(ASN1_Class_UNIVERSAL):
+ name="X509"
+ CONT0 = 0xa0
+ CONT1 = 0xa1
+ # [...]
+
+All ASN.1 objects are represented by simple Python instances that act as nutshells for the raw values. The simple logic is handled by ``ASN1_Object`` whose they inherit from. Hence they are quite simple::
+
+ class ASN1_INTEGER(ASN1_Object):
+ tag = ASN1_Class_UNIVERSAL.INTEGER
+
+ class ASN1_STRING(ASN1_Object):
+ tag = ASN1_Class_UNIVERSAL.STRING
+
+ class ASN1_BIT_STRING(ASN1_STRING):
+ tag = ASN1_Class_UNIVERSAL.BIT_STRING
+
+These instances can be assembled to create an ASN.1 tree::
+
+ >>> x=ASN1_SEQUENCE([ASN1_INTEGER(7),ASN1_STRING("egg"),ASN1_SEQUENCE([ASN1_BOOLEAN(False)])])
+ >>> x
+ , , ]]>]]>
+ >>> x.show()
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SEQUENCE:
+
+
+Encoding engines
+^^^^^^^^^^^^^^^^^
+
+As with the standard, ASN.1 and encoding are independent. We have just seen how to create a compounded ASN.1 object. To encode or decode it, we need to choose an encoding rule. Scapy provides only BER for the moment (actually, it may be DER. DER looks like BER except only minimal encoding is authorised which may well be what I did). I call this an ASN.1 codec.
+
+Encoding and decoding are done using class methods provided by the codec. For example the ``BERcodec_INTEGER`` class provides a ``.enc()`` and a ``.dec()`` class methods that can convert between an encoded string and a value of their type. They all inherit from BERcodec_Object which is able to decode objects from any type::
+
+ >>> BERcodec_INTEGER.enc(7)
+ '\x02\x01\x07'
+ >>> BERcodec_BIT_STRING.enc("egg")
+ '\x03\x03egg'
+ >>> BERcodec_STRING.enc("egg")
+ '\x04\x03egg'
+ >>> BERcodec_STRING.dec('\x04\x03egg')
+ (, '')
+ >>> BERcodec_STRING.dec('\x03\x03egg')
+ Traceback (most recent call last):
+ File "", line 1, in ?
+ File "/usr/bin/scapy", line 2099, in dec
+ return cls.do_dec(s, context, safe)
+ File "/usr/bin/scapy", line 2178, in do_dec
+ l,s,t = cls.check_type_check_len(s)
+ File "/usr/bin/scapy", line 2076, in check_type_check_len
+ l,s3 = cls.check_type_get_len(s)
+ File "/usr/bin/scapy", line 2069, in check_type_get_len
+ s2 = cls.check_type(s)
+ File "/usr/bin/scapy", line 2065, in check_type
+ (cls.__name__, ord(s[0]), ord(s[0]),cls.tag), remaining=s)
+ BER_BadTag_Decoding_Error: BERcodec_STRING: Got tag [3/0x3] while expecting
+ ### Already decoded ###
+ None
+ ### Remaining ###
+ '\x03\x03egg'
+ >>> BERcodec_Object.dec('\x03\x03egg')
+ (, '')
+
+ASN.1 objects are encoded using their ``.enc()`` method. This method must be called with the codec we want to use. All codecs are referenced in the ASN1_Codecs object. ``raw()`` can also be used. In this case, the default codec (``conf.ASN1_default_codec``) will be used.
+
+::
+
+ >>> x.enc(ASN1_Codecs.BER)
+ '0\r\x02\x01\x07\x04\x03egg0\x03\x01\x01\x00'
+ >>> raw(x)
+ '0\r\x02\x01\x07\x04\x03egg0\x03\x01\x01\x00'
+ >>> xx,remain = BERcodec_Object.dec(_)
+ >>> xx.show()
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SEQUENCE:
+
+
+ >>> remain
+ ''
+
+By default, decoding is done using the ``Universal`` class, which means objects defined in the ``Context`` class will not be decoded. There is a good reason for that: the decoding depends on the context!
+
+::
+
+ >>> cert="""
+ ... MIIF5jCCA86gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBgzELMAkGA1UEBhMC
+ ... VVMxHTAbBgNVBAoTFEFPTCBUaW1lIFdhcm5lciBJbmMuMRwwGgYDVQQLExNB
+ ... bWVyaWNhIE9ubGluZSBJbmMuMTcwNQYDVQQDEy5BT0wgVGltZSBXYXJuZXIg
+ ... Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAyMB4XDTAyMDUyOTA2MDAw
+ ... MFoXDTM3MDkyODIzNDMwMFowgYMxCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRB
+ ... T0wgVGltZSBXYXJuZXIgSW5jLjEcMBoGA1UECxMTQW1lcmljYSBPbmxpbmUg
+ ... SW5jLjE3MDUGA1UEAxMuQU9MIFRpbWUgV2FybmVyIFJvb3QgQ2VydGlmaWNh
+ ... dGlvbiBBdXRob3JpdHkgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
+ ... ggIBALQ3WggWmRToVbEbJGv8x4vmh6mJ7ouZzU9AhqS2TcnZsdw8TQ2FTBVs
+ ... RotSeJ/4I/1n9SQ6aF3Q92RhQVSji6UI0ilbm2BPJoPRYxJWSXakFsKlnUWs
+ ... i4SVqBax7J/qJBrvuVdcmiQhLE0OcR+mrF1FdAOYxFSMFkpBd4aVdQxHAWZg
+ ... /BXxD+r1FHjHDtdugRxev17nOirYlxcwfACtCJ0zr7iZYYCLqJV+FNwSbKTQ
+ ... 2O9ASQI2+W6p1h2WVgSysy0WVoaP2SBXgM1nEG2wTPDaRrbqJS5Gr42whTg0
+ ... ixQmgiusrpkLjhTXUr2eacOGAgvqdnUxCc4zGSGFQ+aJLZ8lN2fxI2rSAG2X
+ ... +Z/nKcrdH9cG6rjJuQkhn8g/BsXS6RJGAE57COtCPStIbp1n3UsC5ETzkxml
+ ... J85per5n0/xQpCyrw2u544BMzwVhSyvcG7mm0tCq9Stz+86QNZ8MUhy/XCFh
+ ... EVsVS6kkUfykXPcXnbDS+gfpj1bkGoxoigTTfFrjnqKhynFbotSg5ymFXQNo
+ ... Kk/SBtc9+cMDLz9l+WceR0DTYw/j1Y75hauXTLPXJuuWCpTehTacyH+BCQJJ
+ ... Kg71ZDIMgtG6aoIbs0t0EfOMd9afv9w3pKdVBC/UMejTRrkDfNoSTllkt1Ex
+ ... MVCgyhwn2RAurda9EGYrw7AiShJbAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMB
+ ... Af8wHQYDVR0OBBYEFE9pbQN+nZ8HGEO8txBO1b+pxCAoMB8GA1UdIwQYMBaA
+ ... FE9pbQN+nZ8HGEO8txBO1b+pxCAoMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG
+ ... 9w0BAQUFAAOCAgEAO/Ouyuguh4X7ZVnnrREUpVe8WJ8kEle7+z802u6teio0
+ ... cnAxa8cZmIDJgt43d15Ui47y6mdPyXSEkVYJ1eV6moG2gcKtNuTxVBFT8zRF
+ ... ASbI5Rq8NEQh3q0l/HYWdyGQgJhXnU7q7C+qPBR7V8F+GBRn7iTGvboVsNIY
+ ... vbdVgaxTwOjdaRITQrcCtQVBynlQboIOcXKTRuidDV29rs4prWPVVRaAMCf/
+ ... drr3uNZK49m1+VLQTkCpx+XCMseqdiThawVQ68W/ClTluUI8JPu3B5wwn3la
+ ... 5uBAUhX0/Kr0VvlEl4ftDmVyXr4m+02kLQgH3thcoNyBM5kYJRF3p+v9WAks
+ ... mWsbivNSPxpNSGDxoPYzAlOL7SUJuA0t7Zdz7NeWH45gDtoQmy8YJPamTQr5
+ ... O8t1wswvziRpyQoijlmn94IM19drNZxDAGrElWe6nEXLuA4399xOAU++CrYD
+ ... 062KRffaJ00psUjf5BHklka9bAI+1lHIlRcBFanyqqryvy9lG2/QuRqT9Y41
+ ... xICHPpQvZuTpqP9BnHAqTyo5GJUefvthATxRCC4oGKQWDzH9OmwjkyB24f0H
+ ... hdFbP9IcczLd+rn4jM8Ch3qaluTtT4mNU0OrDhPAARW0eTjb/G49nlG2uBOL
+ ... Z8/5fNkiHfZdxRwBL5joeiQYvITX+txyW/fBOmg=
+ ... """.decode("base64")
+ >>> (dcert,remain) = BERcodec_Object.dec(cert)
+ Traceback (most recent call last):
+ File "", line 1, in ?
+ File "/usr/bin/scapy", line 2099, in dec
+ return cls.do_dec(s, context, safe)
+ File "/usr/bin/scapy", line 2094, in do_dec
+ return codec.dec(s,context,safe)
+ File "/usr/bin/scapy", line 2099, in dec
+ return cls.do_dec(s, context, safe)
+ File "/usr/bin/scapy", line 2218, in do_dec
+ o,s = BERcodec_Object.dec(s, context, safe)
+ File "/usr/bin/scapy", line 2099, in dec
+ return cls.do_dec(s, context, safe)
+ File "/usr/bin/scapy", line 2094, in do_dec
+ return codec.dec(s,context,safe)
+ File "/usr/bin/scapy", line 2099, in dec
+ return cls.do_dec(s, context, safe)
+ File "/usr/bin/scapy", line 2218, in do_dec
+ o,s = BERcodec_Object.dec(s, context, safe)
+ File "/usr/bin/scapy", line 2099, in dec
+ return cls.do_dec(s, context, safe)
+ File "/usr/bin/scapy", line 2092, in do_dec
+ raise BER_Decoding_Error("Unknown prefix [%02x] for [%r]" % (p,t), remaining=s)
+ BER_Decoding_Error: Unknown prefix [a0] for ['\xa0\x03\x02\x01\x02\x02\x01\x010\r\x06\t*\x86H...']
+ ### Already decoded ###
+ [[]]
+ ### Remaining ###
+ '\xa0\x03\x02\x01\x02\x02\x01\x010\r\x06\t*\x86H\x86\xf7\r\x01\x01\x05\x05\x000\x81\x831\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x1d0\x1b\x06\x03U\x04\n\x13\x14AOL Time Warner Inc.1\x1c0\x1a\x06\x03U\x04\x0b\x13\x13America Online Inc.1705\x06\x03U\x04\x03\x13.AOL Time Warner Root Certification Authority 20\x1e\x17\r020529060000Z\x17\r370928234300Z0\x81\x831\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x1d0\x1b\x06\x03U\x04\n\x13\x14AOL Time Warner Inc.1\x1c0\x1a\x06\x03U\x04\x0b\x13\x13America Online Inc.1705\x06\x03U\x04\x03\x13.AOL Time Warner Root Certification Authority 20\x82\x02"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x02\x0f\x000\x82\x02\n\x02\x82\x02\x01\x00\xb47Z\x08\x16\x99\x14\xe8U\xb1\x1b$k\xfc\xc7\x8b\xe6\x87\xa9\x89\xee\x8b\x99\xcdO@\x86\xa4\xb6M\xc9\xd9\xb1\xdc\xd6Q\xc8\x95\x17\x01\x15\xa9\xf2\xaa\xaa\xf2\xbf/e\x1bo\xd0\xb9\x1a\x93\xf5\x8e5\xc4\x80\x87>\x94/f\xe4\xe9\xa8\xffA\x9cp*O*9\x18\x95\x1e~\xfba\x01>> (dcert,remain) = BERcodec_Object.dec(cert, context=ASN1_Class_X509)
+ >>> dcert.show()
+ # ASN1_SEQUENCE:
+ # ASN1_SEQUENCE:
+ # ASN1_X509_CONT0:
+
+
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SEQUENCE:
+ # ASN1_SET:
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SET:
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SET:
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SET:
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SEQUENCE:
+ # ASN1_SET:
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SET:
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SET:
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SET:
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SEQUENCE:
+ # ASN1_SEQUENCE:
+
+
+
+ # ASN1_X509_CONT3:
+ # ASN1_SEQUENCE:
+ # ASN1_SEQUENCE:
+
+
+
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SEQUENCE:
+
+
+ # ASN1_SEQUENCE:
+
+
+
+ # ASN1_SEQUENCE:
+
+
+ \xd6Q\xc8\x95\x17\x01\x15\xa9\xf2\xaa\xaa\xf2\xbf/e\x1bo\xd0\xb9\x1a\x93\xf5\x8e5\xc4\x80\x87>\x94/f\xe4\xe9\xa8\xffA\x9cp*O*9\x18\x95\x1e~\xfba\x01
+
+ASN.1 layers
+^^^^^^^^^^^^
+
+While this may be nice, it's only an ASN.1 encoder/decoder. Nothing related to Scapy yet.
+
+ASN.1 fields
+~~~~~~~~~~~~
+
+Scapy provides ASN.1 fields. They will wrap ASN.1 objects and provide the necessary logic to bind a field name to the value. ASN.1 packets will be described as a tree of ASN.1 fields. Then each field name will be made available as a normal ``Packet`` object, in a flat flavor (ex: to access the version field of a SNMP packet, you don't need to know how many containers wrap it).
+
+Each ASN.1 field is linked to an ASN.1 object through its tag.
+
+
+ASN.1 packets
+~~~~~~~~~~~~~
+
+ASN.1 packets inherit from the Packet class. Instead of a ``fields_desc`` list of fields, they define ``ASN1_codec`` and ``ASN1_root`` attributes. The first one is a codec (for example: ``ASN1_Codecs.BER``), the second one is a tree compounded with ASN.1 fields.
+
+A complete example: SNMP
+------------------------
+
+SNMP defines new ASN.1 objects. We need to define them::
+
+ class ASN1_Class_SNMP(ASN1_Class_UNIVERSAL):
+ name="SNMP"
+ PDU_GET = 0xa0
+ PDU_NEXT = 0xa1
+ PDU_RESPONSE = 0xa2
+ PDU_SET = 0xa3
+ PDU_TRAPv1 = 0xa4
+ PDU_BULK = 0xa5
+ PDU_INFORM = 0xa6
+ PDU_TRAPv2 = 0xa7
+
+These objects are PDU, and are in fact new names for a sequence container (this is generally the case for context objects: they are old containers with new names). This means creating the corresponding ASN.1 objects and BER codecs is simplistic::
+
+ class ASN1_SNMP_PDU_GET(ASN1_SEQUENCE):
+ tag = ASN1_Class_SNMP.PDU_GET
+
+ class ASN1_SNMP_PDU_NEXT(ASN1_SEQUENCE):
+ tag = ASN1_Class_SNMP.PDU_NEXT
+
+ # [...]
+
+ class BERcodec_SNMP_PDU_GET(BERcodec_SEQUENCE):
+ tag = ASN1_Class_SNMP.PDU_GET
+
+ class BERcodec_SNMP_PDU_NEXT(BERcodec_SEQUENCE):
+ tag = ASN1_Class_SNMP.PDU_NEXT
+
+ # [...]
+
+Metaclasses provide the magic behind the fact that everything is automatically registered and that ASN.1 objects and BER codecs can find each other.
+
+The ASN.1 fields are also trivial::
+
+ class ASN1F_SNMP_PDU_GET(ASN1F_SEQUENCE):
+ ASN1_tag = ASN1_Class_SNMP.PDU_GET
+
+ class ASN1F_SNMP_PDU_NEXT(ASN1F_SEQUENCE):
+ ASN1_tag = ASN1_Class_SNMP.PDU_NEXT
+
+ # [...]
+
+Now, the hard part, the ASN.1 packet::
+
+ SNMP_error = { 0: "no_error",
+ 1: "too_big",
+ # [...]
+ }
+
+ SNMP_trap_types = { 0: "cold_start",
+ 1: "warm_start",
+ # [...]
+ }
+
+ class SNMPvarbind(ASN1_Packet):
+ ASN1_codec = ASN1_Codecs.BER
+ ASN1_root = ASN1F_SEQUENCE( ASN1F_OID("oid","1.3"),
+ ASN1F_field("value",ASN1_NULL(0))
+ )
+
+
+ class SNMPget(ASN1_Packet):
+ ASN1_codec = ASN1_Codecs.BER
+ ASN1_root = ASN1F_SNMP_PDU_GET( ASN1F_INTEGER("id",0),
+ ASN1F_enum_INTEGER("error",0, SNMP_error),
+ ASN1F_INTEGER("error_index",0),
+ ASN1F_SEQUENCE_OF("varbindlist", [], SNMPvarbind)
+ )
+
+ class SNMPnext(ASN1_Packet):
+ ASN1_codec = ASN1_Codecs.BER
+ ASN1_root = ASN1F_SNMP_PDU_NEXT( ASN1F_INTEGER("id",0),
+ ASN1F_enum_INTEGER("error",0, SNMP_error),
+ ASN1F_INTEGER("error_index",0),
+ ASN1F_SEQUENCE_OF("varbindlist", [], SNMPvarbind)
+ )
+ # [...]
+
+ class SNMP(ASN1_Packet):
+ ASN1_codec = ASN1_Codecs.BER
+ ASN1_root = ASN1F_SEQUENCE(
+ ASN1F_enum_INTEGER("version", 1, {0:"v1", 1:"v2c", 2:"v2", 3:"v3"}),
+ ASN1F_STRING("community","public"),
+ ASN1F_CHOICE("PDU", SNMPget(),
+ SNMPget, SNMPnext, SNMPresponse, SNMPset,
+ SNMPtrapv1, SNMPbulk, SNMPinform, SNMPtrapv2)
+ )
+ def answers(self, other):
+ return ( isinstance(self.PDU, SNMPresponse) and
+ ( isinstance(other.PDU, SNMPget) or
+ isinstance(other.PDU, SNMPnext) or
+ isinstance(other.PDU, SNMPset) ) and
+ self.PDU.id == other.PDU.id )
+ # [...]
+ bind_layers( UDP, SNMP, sport=161)
+ bind_layers( UDP, SNMP, dport=161)
+
+That wasn't that much difficult. If you think that can't be that short to implement SNMP encoding/decoding and that I may have cut too much, just look at the complete source code.
+
+Now, how to use it? As usual::
+
+ >>> a=SNMP(version=3, PDU=SNMPget(varbindlist=[SNMPvarbind(oid="1.2.3",value=5),
+ ... SNMPvarbind(oid="3.2.1",value="hello")]))
+ >>> a.show()
+ ###[ SNMP ]###
+ version= v3
+ community= 'public'
+ \PDU\
+ |###[ SNMPget ]###
+ | id= 0
+ | error= no_error
+ | error_index= 0
+ | \varbindlist\
+ | |###[ SNMPvarbind ]###
+ | | oid= '1.2.3'
+ | | value= 5
+ | |###[ SNMPvarbind ]###
+ | | oid= '3.2.1'
+ | | value= 'hello'
+ >>> hexdump(a)
+ 0000 30 2E 02 01 03 04 06 70 75 62 6C 69 63 A0 21 02 0......public.!.
+ 0010 01 00 02 01 00 02 01 00 30 16 30 07 06 02 2A 03 ........0.0...*.
+ 0020 02 01 05 30 0B 06 02 7A 01 04 05 68 65 6C 6C 6F ...0...z...hello
+ >>> send(IP(dst="1.2.3.4")/UDP()/SNMP())
+ .
+ Sent 1 packets.
+ >>> SNMP(raw(a)).show()
+ ###[ SNMP ]###
+ version=
+ community=
+ \PDU\
+ |###[ SNMPget ]###
+ | id=
+ | error=
+ | error_index=
+ | \varbindlist\
+ | |###[ SNMPvarbind ]###
+ | | oid=
+ | | value=
+ | |###[ SNMPvarbind ]###
+ | | oid=
+ | | value=
+
+
+
+Resolving OID from a MIB
+------------------------
+
+About OID objects
+^^^^^^^^^^^^^^^^^
+
+OID objects are created with an ``ASN1_OID`` class::
+
+ >>> o1=ASN1_OID("2.5.29.10")
+ >>> o2=ASN1_OID("1.2.840.113549.1.1.1")
+ >>> o1,o2
+ (, )
+
+Loading a MIB
+^^^^^^^^^^^^^
+
+Scapy can parse MIB files and become aware of a mapping between an OID and its name::
+
+ >>> load_mib("mib/*")
+ >>> o1,o2
+ (, )
+
+The MIB files I've used are attached to this page.
+
+Scapy's MIB database
+^^^^^^^^^^^^^^^^^^^^
+
+All MIB information is stored into the conf.mib object. This object can be used to find the OID of a name
+
+::
+
+ >>> conf.mib.sha1_with_rsa_signature
+ '1.2.840.113549.1.1.5'
+
+or to resolve an OID::
+
+ >>> conf.mib._oidname("1.2.3.6.1.4.1.5")
+ 'enterprises.5'
+
+It is even possible to graph it::
+
+ >>> conf.mib._make_graph()
\ No newline at end of file
diff --git a/doc/scapy/advanced_usage/automaton.rst b/doc/scapy/advanced_usage/automaton.rst
new file mode 100644
index 00000000000..b8a7e984d70
--- /dev/null
+++ b/doc/scapy/advanced_usage/automaton.rst
@@ -0,0 +1,376 @@
+Automata
+========
+
+Scapy enables to create easily network automata. Scapy does not stick to a specific model like Moore or Mealy automata. It provides a flexible way for you to choose your way to go.
+
+An automaton in Scapy is deterministic. It has different states. A start state and some end and error states. There are transitions from one state to another. Transitions can be transitions on a specific condition, transitions on the reception of a specific packet or transitions on a timeout. When a transition is taken, one or more actions can be run. An action can be bound to many transitions. Parameters can be passed from states to transitions, and from transitions to states and actions.
+
+From a programmer's point of view, states, transitions and actions are methods from an Automaton subclass. They are decorated to provide meta-information needed in order for the automaton to work.
+
+First example
+-------------
+
+Let's begin with a simple example. I take the convention to write states with capitals, but anything valid with Python syntax would work as well.
+
+::
+
+ class HelloWorld(Automaton):
+ @ATMT.state(initial=1)
+ def BEGIN(self):
+ print("State=BEGIN")
+
+ @ATMT.condition(BEGIN)
+ def wait_for_nothing(self):
+ print("Wait for nothing...")
+ raise self.END()
+
+ @ATMT.action(wait_for_nothing)
+ def on_nothing(self):
+ print("Action on 'nothing' condition")
+
+ @ATMT.state(final=1)
+ def END(self):
+ print("State=END")
+
+In this example, we can see 3 decorators:
+
+* ``ATMT.state`` that is used to indicate that a method is a state, and that can
+ have initial, final, stop and error optional arguments set to non-zero for special states.
+* ``ATMT.condition`` that indicate a method to be run when the automaton state
+ reaches the indicated state. The argument is the name of the method representing that state
+* ``ATMT.action`` binds a method to a transition and is run when the transition is taken.
+
+Running this example gives the following result::
+
+ >>> a=HelloWorld()
+ >>> a.run()
+ State=BEGIN
+ Wait for nothing...
+ Action on 'nothing' condition
+ State=END
+ >>> a.destroy()
+
+This simple automaton can be described with the following graph:
+
+.. image:: ../graphics/ATMT_HelloWorld.*
+
+The graph can be automatically drawn from the code with::
+
+ >>> HelloWorld.graph()
+
+.. note:: An ``Automaton`` can be reset using ``restart()``. It is then possible to run it again.
+
+.. warning:: Remember to call ``destroy()`` once you're done using an Automaton. (especially on PyPy)
+
+Changing states
+---------------
+
+The ``ATMT.state`` decorator transforms a method into a function that returns an exception. If you raise that exception, the automaton state will be changed. If the change occurs in a transition, actions bound to this transition will be called. The parameters given to the function replacing the method will be kept and finally delivered to the method. The exception has a method action_parameters that can be called before it is raised so that it will store parameters to be delivered to all actions bound to the current transition.
+
+As an example, let's consider the following state::
+
+ @ATMT.state()
+ def MY_STATE(self, param1, param2):
+ print("state=MY_STATE. param1=%r param2=%r" % (param1, param2))
+
+This state will be reached with the following code::
+
+ @ATMT.receive_condition(ANOTHER_STATE)
+ def received_ICMP(self, pkt):
+ if ICMP in pkt:
+ raise self.MY_STATE("got icmp", pkt[ICMP].type)
+
+Let's suppose we want to bind an action to this transition, that will also need some parameters::
+
+ @ATMT.action(received_ICMP)
+ def on_ICMP(self, icmp_type, icmp_code):
+ self.retaliate(icmp_type, icmp_code)
+
+The condition should become::
+
+ @ATMT.receive_condition(ANOTHER_STATE)
+ def received_ICMP(self, pkt):
+ if ICMP in pkt:
+ raise self.MY_STATE("got icmp", pkt[ICMP].type).action_parameters(pkt[ICMP].type, pkt[ICMP].code)
+
+Real example
+------------
+
+Here is a real example take from Scapy. It implements a TFTP client that can issue read requests.
+
+.. image:: ../graphics/ATMT_TFTP_read.*
+
+::
+
+ class TFTP_read(Automaton):
+ def parse_args(self, filename, server, sport = None, port=69, **kargs):
+ Automaton.parse_args(self, **kargs)
+ self.filename = filename
+ self.server = server
+ self.port = port
+ self.sport = sport
+
+ def master_filter(self, pkt):
+ return ( IP in pkt and pkt[IP].src == self.server and UDP in pkt
+ and pkt[UDP].dport == self.my_tid
+ and (self.server_tid is None or pkt[UDP].sport == self.server_tid) )
+
+ # BEGIN
+ @ATMT.state(initial=1)
+ def BEGIN(self):
+ self.blocksize=512
+ self.my_tid = self.sport or RandShort()._fix()
+ bind_bottom_up(UDP, TFTP, dport=self.my_tid)
+ self.server_tid = None
+ self.res = b""
+
+ self.l3 = IP(dst=self.server)/UDP(sport=self.my_tid, dport=self.port)/TFTP()
+ self.last_packet = self.l3/TFTP_RRQ(filename=self.filename, mode="octet")
+ self.send(self.last_packet)
+ self.awaiting=1
+
+ raise self.WAITING()
+
+ # WAITING
+ @ATMT.state()
+ def WAITING(self):
+ pass
+
+ @ATMT.receive_condition(WAITING)
+ def receive_data(self, pkt):
+ if TFTP_DATA in pkt and pkt[TFTP_DATA].block == self.awaiting:
+ if self.server_tid is None:
+ self.server_tid = pkt[UDP].sport
+ self.l3[UDP].dport = self.server_tid
+ raise self.RECEIVING(pkt)
+ @ATMT.action(receive_data)
+ def send_ack(self):
+ self.last_packet = self.l3 / TFTP_ACK(block = self.awaiting)
+ self.send(self.last_packet)
+
+ @ATMT.receive_condition(WAITING, prio=1)
+ def receive_error(self, pkt):
+ if TFTP_ERROR in pkt:
+ raise self.ERROR(pkt)
+
+ @ATMT.timeout(WAITING, 3)
+ def timeout_waiting(self):
+ raise self.WAITING()
+ @ATMT.action(timeout_waiting)
+ def retransmit_last_packet(self):
+ self.send(self.last_packet)
+
+ # RECEIVED
+ @ATMT.state()
+ def RECEIVING(self, pkt):
+ recvd = pkt[Raw].load
+ self.res += recvd
+ self.awaiting += 1
+ if len(recvd) == self.blocksize:
+ raise self.WAITING()
+ raise self.END()
+
+ # ERROR
+ @ATMT.state(error=1)
+ def ERROR(self,pkt):
+ split_bottom_up(UDP, TFTP, dport=self.my_tid)
+ return pkt[TFTP_ERROR].summary()
+
+ #END
+ @ATMT.state(final=1)
+ def END(self):
+ split_bottom_up(UDP, TFTP, dport=self.my_tid)
+ return self.res
+
+It can be run like this, for instance::
+
+ >>> atmt = TFTP_read("my_file", "192.168.1.128")
+ >>> atmt.run()
+ >>> atmt.destroy()
+
+Detailed documentation
+----------------------
+
+Decorators
+^^^^^^^^^^
+Decorator for states
+~~~~~~~~~~~~~~~~~~~~
+
+States are methods decorated by the result of the ``ATMT.state`` function. It can take 4 optional parameters, ``initial``, ``final``, ``stop`` and ``error``, that, when set to ``True``, indicating that the state is an initial, final, stop or error state.
+
+.. note:: The ``initial`` state is called while starting the automata. The ``final`` step will tell the automata has reached its end. If you call ``atmt.stop()``, the automata will move to the ``stop`` step whatever its current state is. The ``error`` state will mark the automata as errored. If no ``stop`` state is specified, calling ``stop`` and ``forcestop`` will be equivalent.
+
+::
+
+ class Example(Automaton):
+ @ATMT.state(initial=1)
+ def BEGIN(self):
+ pass
+
+ @ATMT.state()
+ def SOME_STATE(self):
+ pass
+
+ @ATMT.state(final=1)
+ def END(self):
+ return "Result of the automaton: 42"
+
+ @ATMT.state(stop=1)
+ def STOP(self):
+ print("SHUTTING DOWN...")
+ # e.g. close sockets...
+
+ @ATMT.condition(STOP)
+ def is_stopping(self):
+ raise self.END()
+
+ @ATMT.state(error=1)
+ def ERROR(self):
+ return "Partial result, or explanation"
+ # [...]
+
+Take for instance the TCP client:
+
+.. image:: ../graphics/ATMT_TCP_client.svg
+
+The ``START`` event is ``initial=1``, the ``STOP`` event is ``stop=1`` and the ``CLOSED`` event is ``final=1``.
+
+Decorators for transitions
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Transitions are methods decorated by the result of one of ``ATMT.condition``, ``ATMT.receive_condition``, ``ATMT.eof``, ``ATMT.timeout``, ``ATMT.timer``. They all take as argument the state method they are related to. ``ATMT.timeout`` and ``ATMT.timer`` also have a mandatory ``timeout`` parameter to provide the timeout value in seconds. The difference between ``ATMT.timeout`` and ``ATMT.timer`` is that ``ATMT.timeout`` gets triggered only once. ``ATMT.timer`` get reloaded automatically, which is useful for sending keep-alive packets. ``ATMT.condition`` and ``ATMT.receive_condition`` have an optional ``prio`` parameter so that the order in which conditions are evaluated can be forced. The default priority is 0. Transitions with the same priority level are called in an undetermined order.
+
+When the automaton switches to a given state, the state's method is executed. Then transitions methods are called at specific moments until one triggers a new state (something like ``raise self.MY_NEW_STATE()``). First, right after the state's method returns, the ``ATMT.condition`` decorated methods are run by growing prio. Then each time a packet is received and accepted by the master filter all ``ATMT.receive_condition`` decorated hods are called by growing prio. When a timeout is reached since the time we entered into the current space, the corresponding ``ATMT.timeout`` decorated method is called. If the socket raises an ``EOFError`` (closed) during a state, the ``ATMT.EOF`` transition is called. Otherwise it raises an exception and the automaton exits.
+
+::
+
+ class Example(Automaton):
+ @ATMT.state()
+ def WAITING(self):
+ pass
+
+ @ATMT.condition(WAITING)
+ def it_is_raining(self):
+ if not self.have_umbrella:
+ raise self.ERROR_WET()
+
+ @ATMT.receive_condition(WAITING, prio=1)
+ def it_is_ICMP(self, pkt):
+ if ICMP in pkt:
+ raise self.RECEIVED_ICMP(pkt)
+
+ @ATMT.receive_condition(WAITING, prio=2)
+ def it_is_IP(self, pkt):
+ if IP in pkt:
+ raise self.RECEIVED_IP(pkt)
+
+ @ATMT.timeout(WAITING, 10.0)
+ def waiting_timeout(self):
+ raise self.ERROR_TIMEOUT()
+
+Decorator for actions
+~~~~~~~~~~~~~~~~~~~~~
+
+Actions are methods that are decorated by the return of ``ATMT.action`` function. This function takes the transition method it is bound to as first parameter and an optional priority ``prio`` as a second parameter. The default priority is 0. An action method can be decorated many times to be bound to many transitions.
+
+::
+
+ from random import random
+
+ class Example(Automaton):
+ @ATMT.state(initial=1)
+ def BEGIN(self):
+ pass
+
+ @ATMT.state(final=1)
+ def END(self):
+ pass
+
+ @ATMT.condition(BEGIN, prio=1)
+ def maybe_go_to_end(self):
+ if random() > 0.5:
+ raise self.END()
+
+ @ATMT.condition(BEGIN, prio=2)
+ def certainly_go_to_end(self):
+ raise self.END()
+
+ @ATMT.action(maybe_go_to_end)
+ def maybe_action(self):
+ print("We are lucky...")
+
+ @ATMT.action(certainly_go_to_end)
+ def certainly_action(self):
+ print("We are not lucky...")
+
+ @ATMT.action(maybe_go_to_end, prio=1)
+ @ATMT.action(certainly_go_to_end, prio=1)
+ def always_action(self):
+ print("This wasn't luck!...")
+
+The two possible outputs are::
+
+ >>> a=Example()
+ >>> a.run()
+ We are not lucky...
+ This wasn't luck!...
+ >>> a.run()
+ We are lucky...
+ This wasn't luck!...
+ >>> a.destroy()
+
+
+.. note:: If you want to pass a parameter to an action, you can use the ``action_parameters`` function while raising the next state.
+
+In the following example, the ``send_copy`` action takes a parameter passed by ``is_fin``::
+
+ class Example(Automaton):
+ @ATMT.state()
+ def WAITING(self):
+ pass
+
+ @ATMT.state()
+ def FIN_RECEIVED(self):
+ pass
+
+ @ATMT.receive_condition(WAITING)
+ def is_fin(self, pkt):
+ if pkt[TCP].flags.F:
+ raise self.FIN_RECEIVED().action_parameters(pkt)
+
+ @ATMT.action(is_fin)
+ def send_copy(self, pkt):
+ send(pkt)
+
+
+Methods to overload
+^^^^^^^^^^^^^^^^^^^
+
+Two methods are hooks to be overloaded:
+
+* The ``parse_args()`` method is called with arguments given at ``__init__()`` and ``run()``. Use that to parametrize the behavior of your automaton.
+
+* The ``master_filter()`` method is called each time a packet is sniffed and decides if it is interesting for the automaton. When working on a specific protocol, this is where you will ensure the packet belongs to the connection you are being part of, so that you do not need to make all the sanity checks in each transition.
+
+Timer configuration
+^^^^^^^^^^^^^^^^^^^
+
+Some protocols allow timer configuration. In order to configure timeout values during class initialization one may use ``timer_by_name()`` method, which returns ``Timer`` object associated with the given function name::
+
+ class Example(Automaton):
+ def __init__(self, *args, **kwargs):
+ super(Example, self).__init__(*args, **kwargs)
+ timer = self.timer_by_name("waiting_timeout")
+ timer.set(1)
+
+ @ATMT.state(initial=1)
+ def WAITING(self):
+ pass
+
+ @ATMT.state(final=1)
+ def END(self):
+ pass
+
+ @ATMT.timeout(WAITING, 10.0)
+ def waiting_timeout(self):
+ raise self.END()
\ No newline at end of file
diff --git a/doc/scapy/fwdmachine.rst b/doc/scapy/advanced_usage/fwdmachine.rst
similarity index 82%
rename from doc/scapy/fwdmachine.rst
rename to doc/scapy/advanced_usage/fwdmachine.rst
index ce8b3ba1a3e..ed26929a729 100644
--- a/doc/scapy/fwdmachine.rst
+++ b/doc/scapy/advanced_usage/fwdmachine.rst
@@ -2,10 +2,10 @@
Forwarding Machine
******************
-Scapy's ``ForwardingMachine`` is a class utility that allows to very quickly design a server that forwards packets to another server, with the ability
-to modify them on-the-fly. This is commonly referred to as a "transparent proxy". The ``ForwardingMachine`` was initially designed to be used with TPROXY,
-a linux feature that allows to bind a socket that received *packets to any IP destination*, in which case it properly forwards the packet to the initially
-intended destination.
+Scapy's ``ForwardingMachine`` is a utility that allows to create server that forwards packets to another server, with the ability
+to modify them on-the-fly. This is similar to a "proxy", but works with any protocols over IP/IPv6. The ``ForwardingMachine`` was initially designed to be used with TPROXY,
+a linux feature that allows to bind a socket that received *packets to any IP destination* (in which case it properly forwards the packet to the initially
+intended destination), but it also work as a standalone server.
A ``ForwardingMachine`` is expected to be used over a normal Python socket, of any kind, and needs to extended with two
functions: ``xfrmcs`` and ``xfrmsc``. The first one is called whenever data is received from the client side (client-to-server), the other when the data
@@ -15,7 +15,7 @@ Basic usage
___________
Here's an example of a ``ForwardingMachine`` over TPROXY that does nothing. Packets for all destinations are handled, and forwarded to their
-initial destinations afterwards. More details on how to setup TPROXY are provided below. Note that a ``ForwardingMachine`` **also works without TPROXY**.
+initial destinations afterwards. More details on how to setup TPROXY are provided below.
.. code:: python
@@ -62,7 +62,7 @@ ___________
``ForwardingMachine`` has support for TLS through the ``ssl=True`` argument. When TLS is enabled, the SNI (Server Name Indication) is
properly forwarded to the remote peer, and can be accessed through the ``ctx.tls_sni_name`` attribute in the callbacks.
-**By default, a ``ForwardingMachine`` generates self-signed certificates** that copy the attributes from the certificate of the remote
+**By default, a ForwardingMachine generates self-signed certificates** that copy the attributes from the certificate of the remote
server. This behavior can be changed by specifying a certificate (which will be served by the TLS stack).
.. code:: python
@@ -85,4 +85,9 @@ server. This behavior can be changed by specifying a certificate (which will be
port=443,
cls=HTTP,
ssl=True,
- ).run()
\ No newline at end of file
+ ).run()
+
+Configuring TPROXY
+__________________
+
+For ease of use, a script
\ No newline at end of file
diff --git a/doc/scapy/advanced_usage/index.rst b/doc/scapy/advanced_usage/index.rst
new file mode 100644
index 00000000000..0e617423fc6
--- /dev/null
+++ b/doc/scapy/advanced_usage/index.rst
@@ -0,0 +1,10 @@
+.. Advanced usage documentation
+
+Advanced usage
+==============
+
+.. toctree::
+ :glob:
+ :titlesonly:
+
+ *
\ No newline at end of file
diff --git a/doc/scapy/advanced_usage/pipetools.rst b/doc/scapy/advanced_usage/pipetools.rst
new file mode 100644
index 00000000000..47c92d8616f
--- /dev/null
+++ b/doc/scapy/advanced_usage/pipetools.rst
@@ -0,0 +1,347 @@
+.. _pipetools:
+
+PipeTools
+=========
+
+Scapy's ``pipetool`` is a smart piping system allowing to perform complex stream data management.
+
+The goal is to create a sequence of steps with one or several inputs and one or several outputs, with a bunch of blocks in between.
+PipeTools can handle varied sources of data (and outputs) such as user input, pcap input, sniffing, wireshark...
+A pipe system is implemented by manually linking all its parts. It is possible to dynamically add an element while running or set multiple drains for the same source.
+
+.. note:: Pipetool default objects are located inside ``scapy.pipetool``
+
+Demo: sniff, anonymize, send to Wireshark
+-----------------------------------------
+
+The following code will sniff packets on the default interface, anonymize the source and destination IP addresses and pipe it all into Wireshark. Useful when posting online examples, for instance.
+
+.. code-block:: python3
+
+ source = SniffSource(iface=conf.iface)
+ wire = WiresharkSink()
+ def transf(pkt):
+ if not pkt or IP not in pkt:
+ return pkt
+ pkt[IP].src = "1.1.1.1"
+ pkt[IP].dst = "2.2.2.2"
+ return pkt
+
+ source > TransformDrain(transf) > wire
+ p = PipeEngine(source)
+ p.start()
+ p.wait_and_stop()
+
+The engine is pretty straightforward:
+
+.. image:: graphics/pipetool_demo.svg
+
+Let's run it:
+
+.. image:: graphics/animations/pipetool_demo.gif
+
+Class Types
+-----------
+
+There are 3 different class of objects used for data management:
+
+- ``Sources``
+- ``Drains``
+- ``Sinks``
+
+They are executed and handled by a :class:`~scapy.pipetool.PipeEngine` object.
+
+When running, a pipetool engine waits for any available data from the Source, and send it in the Drains linked to it.
+The data then goes from Drains to Drains until it arrives in a Sink, the final state of this data.
+
+Let's see with a basic demo how to build a pipetool system.
+
+.. image:: graphics/pipetool_engine.png
+
+For instance, this engine was generated with this code:
+
+.. code:: pycon
+
+ >>> s = CLIFeeder()
+ >>> s2 = CLIHighFeeder()
+ >>> d1 = Drain()
+ >>> d2 = TransformDrain(lambda x: x[::-1])
+ >>> si1 = ConsoleSink()
+ >>> si2 = QueueSink()
+ >>>
+ >>> s > d1
+ >>> d1 > si1
+ >>> d1 > si2
+ >>>
+ >>> s2 >> d1
+ >>> d1 >> d2
+ >>> d2 >> si1
+ >>>
+ >>> p = PipeEngine()
+ >>> p.add(s)
+ >>> p.add(s2)
+ >>> p.graph(target="> the_above_image.png")
+
+``start()`` is used to start the :class:`~scapy.pipetool.PipeEngine`:
+
+.. code:: pycon
+
+ >>> p.start()
+
+Now, let's play with it by sending some input data
+
+.. code:: pycon
+
+ >>> s.send("foo")
+ >'foo'
+ >>> s2.send("bar")
+ >>'rab'
+ >>> s.send("i like potato")
+ >'i like potato'
+ >>> print(si2.recv(), ":", si2.recv())
+ foo : i like potato
+
+Let's study what happens here:
+
+- there are **two canals** in a :class:`~scapy.pipetool.PipeEngine`, a lower one and a higher one. Some Sources write on the lower one, some on the higher one and some on both.
+- most sources can be linked to any drain, on both lower and higher canals. The use of ``>`` indicates a link on the low canal, and ``>>`` on the higher one.
+- when we send some data in ``s``, which is on the lower canal, as shown above, it goes through the :class:`~scapy.pipetool.Drain` then is sent to the :class:`~.scapy.pipetool.QueueSink` and to the :class:`~scapy.pipetool.ConsoleSink`
+- when we send some data in ``s2``, it goes through the Drain, then the TransformDrain where the data is reversed (see the lambda), before being sent to :class:`~scapy.pipetool.ConsoleSink` only. This explains why we only have the data of the lower sources inside the QueueSink: the higher one has not been linked.
+
+Most of the sinks receive from both lower and upper canals. This is verifiable using the `help(ConsoleSink)`
+
+.. code:: pycon
+
+ >>> help(ConsoleSink)
+ Help on class ConsoleSink in module scapy.pipetool:
+ class ConsoleSink(Sink)
+ | Print messages on low and high entries
+ | +-------+
+ | >>-|--. |->>
+ | | print |
+ | >-|--' |->
+ | +-------+
+ |
+ [...]
+
+
+Sources
+^^^^^^^
+
+A Source is a class that generates some data.
+
+There are several source types integrated with Scapy, usable as-is, but you may
+also create yours.
+
+Default Source classes
+~~~~~~~~~~~~~~~~~~~~~~
+
+For any of those class, have a look at ``help([theclass])`` to get more information or the required parameters.
+
+- :class:`~scapy.pipetool.CLIFeeder` : a source especially used in interactive software. its ``send(data)`` generates the event data on the lower canal
+- :class:`~scapy.pipetool.CLIHighFeeder` : same than CLIFeeder, but writes on the higher canal
+- :class:`~scapy.pipetool.PeriodicSource` : Generate messages periodically on the low canal.
+- :class:`~scapy.pipetool.AutoSource`: the default source, that must be extended to create custom sources.
+
+Create a custom Source
+~~~~~~~~~~~~~~~~~~~~~~
+
+To create a custom source, one must extend the :class:`~scapy.pipetool.AutoSource` class.
+
+.. note::
+
+ Do NOT use the default :class:`~scapy.pipetool.Source` class except if you are really sure of what you are doing: it is only used internally, and is missing some implementation. The :class:`~scapy.pipetool.AutoSource` is made to be used.
+
+
+To send data through it, the object must call its ``self._gen_data(msg)`` or ``self._gen_high_data(msg)`` functions, which send the data into the PipeEngine.
+
+The Source should also (if possible), set ``self.is_exhausted`` to ``True`` when empty, to allow the clean stop of the :class:`~scapy.pipetool.PipeEngine`. If the source is infinite, it will need a force-stop (see PipeEngine below)
+
+For instance, here is how :class:`~scapy.pipetool.CLIHighFeeder` is implemented:
+
+.. code:: python3
+
+ class CLIFeeder(CLIFeeder):
+ def send(self, msg):
+ self._gen_high_data(msg)
+ def close(self):
+ self.is_exhausted = True
+
+Drains
+^^^^^^
+
+Default Drain classes
+~~~~~~~~~~~~~~~~~~~~~
+
+Drains need to be linked on the entry that you are using. It can be either on the lower one (using ``>``) or the upper one (using ``>>``).
+See the basic example above.
+
+- :class:`~scapy.pipetool.Drain` : the most basic Drain possible. Will pass on both low and high entry if linked properly.
+- :class:`~scapy.pipetool.TransformDrain` : Apply a function to messages on low and high entry
+- :class:`~scapy.pipetool.UpDrain` : Repeat messages from low entry to high exit
+- :class:`~scapy.pipetool.DownDrain` : Repeat messages from high entry to low exit
+
+Create a custom Drain
+~~~~~~~~~~~~~~~~~~~~~
+
+To create a custom drain, one must extend the :class:`~scapy.pipetool.Drain` class.
+
+A :class:`~scapy.pipetool.Drain` object will receive data from the lower canal in its ``push`` method, and from the higher canal from its ``high_push`` method.
+
+To send the data back into the next linked Drain / Sink, it must call the ``self._send(msg)`` or ``self._high_send(msg)`` methods.
+
+For instance, here is how :class:`~scapy.pipetool.TransformDrain` is implemented::
+
+ class TransformDrain(Drain):
+ def __init__(self, f, name=None):
+ Drain.__init__(self, name=name)
+ self.f = f
+ def push(self, msg):
+ self._send(self.f(msg))
+ def high_push(self, msg):
+ self._high_send(self.f(msg))
+
+Sinks
+^^^^^
+
+Sinks are destinations for messages.
+
+A :py:class:`~scapy.pipetool.Sink` receives data like a :py:class:`~scapy.pipetool.Drain`, but doesn't send any
+messages after it.
+
+Messages on the low entry come from :py:meth:`~scapy.pipetool.Sink.push`, and messages on the
+high entry come from :py:meth:`~scapy.pipetool.Sink.high_push`.
+
+Default Sinks classes
+~~~~~~~~~~~~~~~~~~~~~
+
+- :class:`~scapy.pipetool.ConsoleSink` : Print messages on low and high entries to ``stdout``
+- :class:`~scapy.pipetool.RawConsoleSink` : Print messages on low and high entries, using os.write
+- :class:`~scapy.pipetool.TermSink` : Prints messages on the low and high entries, on a separate terminal
+- :class:`~scapy.pipetool.QueueSink` : Collects messages on the low and high entries into a :py:class:`Queue`
+
+Create a custom Sink
+~~~~~~~~~~~~~~~~~~~~
+
+To create a custom sink, one must extend :py:class:`~scapy.pipetool.Sink` and implement
+:py:meth:`~scapy.pipetool.Sink.push` and/or :py:meth:`~scapy.pipetool.Sink.high_push`.
+
+This is a simplified version of :py:class:`~scapy.pipetool.ConsoleSink`:
+
+.. code-block:: python3
+
+ class ConsoleSink(Sink):
+ def push(self, msg):
+ print(">%r" % msg)
+ def high_push(self, msg):
+ print(">>%r" % msg)
+
+Link objects
+------------
+
+As shown in the example, most sources can be linked to any drain, on both low
+and high entry.
+
+The use of ``>`` indicates a link on the low entry, and ``>>`` on the high
+entry.
+
+For example, to link ``a``, ``b`` and ``c`` on the low entries:
+
+.. code-block:: pycon
+
+ >>> a = CLIFeeder()
+ >>> b = Drain()
+ >>> c = ConsoleSink()
+ >>> a > b > c
+ >>> p = PipeEngine()
+ >>> p.add(a)
+
+This wouldn't link the high entries, so something like this would do nothing:
+
+.. code-block:: pycon
+
+ >>> a2 = CLIHighFeeder()
+ >>> a2 >> b
+ >>> a2.send("hello")
+
+Because ``b`` (:py:class:`~scapy.pipetool.Drain`) and ``c`` (:py:class:`scapy.pipetool.ConsoleSink`) are not
+linked on the high entry.
+
+However, using a :py:class:`~scapy.pipetool.DownDrain` would bring the high messages from
+:py:class:`~scapy.pipetool.CLIHighFeeder` to the lower channel:
+
+.. code-block:: pycon
+
+ >>> a2 = CLIHighFeeder()
+ >>> b2 = DownDrain()
+ >>> a2 >> b2
+ >>> b2 > b
+ >>> a2.send("hello")
+
+The PipeEngine class
+--------------------
+
+The :class:`~scapy.pipetool.PipeEngine` class is the core class of the Pipetool system. It must be initialized and passed the list of all Sources.
+
+There are two ways of passing sources:
+
+- during initialization: ``p = PipeEngine(source1, source2, ...)``
+- using the ``add(source)`` method
+
+A :class:`~scapy.pipetool.PipeEngine` class must be started with ``.start()`` function. It may be force-stopped with the ``.stop()``, or cleanly stopped with ``.wait_and_stop()``
+
+A clean stop only works if the Sources is exhausted (has no data to send left).
+
+It can be printed into a graph using ``.graph()`` methods. see ``help(do_graph)`` for the list of available keyword arguments.
+
+Scapy advanced PipeTool objects
+-------------------------------
+
+.. note:: Unlike the previous objects, those are not located in ``scapy.pipetool`` but in ``scapy.scapypipes``
+
+Now that you know the default PipeTool objects, here are some more advanced ones, based on packet functionalities.
+
+- :class:`~scapy.scapypipes.SniffSource` : Read packets from an interface and send them to low exit.
+- :class:`~scapy.scapypipes.RdpcapSource` : Read packets from a PCAP file send them to low exit.
+- :class:`~scapy.scapypipes.InjectSink` : Packets received on low input are injected (sent) to an interface
+- :class:`~scapy.scapypipes.WrpcapSink` : Packets received on low input are written to PCAP file
+- :class:`~scapy.scapypipes.UDPDrain` : UDP payloads received on high entry are sent over UDP (complicated, have a look at ``help(UDPDrain)``)
+- :class:`~scapy.scapypipes.FDSourceSink` : Use a file descriptor as source and sink
+- :class:`~scapy.scapypipes.TCPConnectPipe`: TCP connect to addr:port and use it as source and sink
+- :class:`~scapy.scapypipes.TCPListenPipe` : TCP listen on [addr:]port and use the first connection as source and sink (complicated, have a look at ``help(TCPListenPipe)``)
+
+Triggering
+----------
+
+Some special sort of Drains exists: the Trigger Drains.
+
+Trigger Drains are special drains, that on receiving data not only pass it by but also send a "Trigger" input, that is received and handled by the next triggered drain (if it exists).
+
+For example, here is a basic :class:`~scapy.scapypipes.TriggerDrain` usage:
+
+.. code:: pycon
+
+ >>> a = CLIFeeder()
+ >>> d = TriggerDrain(lambda msg: True) # Pass messages and trigger when a condition is met
+ >>> d2 = TriggeredValve()
+ >>> s = ConsoleSink()
+ >>> a > d > d2 > s
+ >>> d ^ d2 # Link the triggers
+ >>> p = PipeEngine(s)
+ >>> p.start()
+ INFO: Pipe engine thread started.
+ >>>
+ >>> a.send("this will be printed")
+ >'this will be printed'
+ >>> a.send("this won't, because the valve was switched")
+ >>> a.send("this will, because the valve was switched again")
+ >'this will, because the valve was switched again'
+ >>> p.stop()
+
+Several triggering Drains exist, they are pretty explicit. It is highly recommended to check the doc using ``help([the class])``
+
+- :class:`~scapy.scapypipes.TriggeredMessage` : Send a preloaded message when triggered and trigger in chain
+- :class:`~scapy.scapypipes.TriggerDrain` : Pass messages and trigger when a condition is met
+- :class:`~scapy.scapypipes.TriggeredValve` : Let messages alternatively pass or not, changing on trigger
+- :class:`~scapy.scapypipes.TriggeredQueueingValve` : Let messages alternatively pass or queued, changing on trigger
+- :class:`~scapy.scapypipes.TriggeredSwitch` : Let messages alternatively high or low, changing on trigger
diff --git a/doc/scapy/graphics/fwdmachine.svg b/doc/scapy/graphics/fwdmachine.svg
index 4cdf6dd1608..e9d06349c43 100644
--- a/doc/scapy/graphics/fwdmachine.svg
+++ b/doc/scapy/graphics/fwdmachine.svg
@@ -1,3 +1,3 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/doc/scapy/index.rst b/doc/scapy/index.rst
index 3fceb6ca558..a7ae7f7d082 100644
--- a/doc/scapy/index.rst
+++ b/doc/scapy/index.rst
@@ -24,7 +24,7 @@ Scapy's documentation is under a `Creative Commons Attribution - Non-Commercial
installation
usage
- advanced_usage
+ advanced_usage/index.rst
routing
.. toctree::
diff --git a/tox.ini b/tox.ini
index 62639d16d3f..d47bddf8b9d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -122,10 +122,9 @@ commands =
# Debug mode
[testenv:docs2]
description = "Build the docs without rebuilding the API tree"
-skip_install = true
+extras = doc
changedir = {toxinidir}/doc/scapy
deps = {[testenv:docs]deps}
-allowlist_externals = sphinx-build
setenv =
SCAPY_APITREE = 0
commands =
From 3675ac3bc37238cad5f8c1a52be1348df2da25c8 Mon Sep 17 00:00:00 2001
From: gpotter2 <10530980+gpotter2@users.noreply.github.com>
Date: Sun, 11 May 2025 23:02:21 +0200
Subject: [PATCH 3/9] Improve naming and forwarding machine
---
doc/scapy/_static/vethrelay.sh | 74 +++++++++++++++++++++
doc/scapy/advanced_usage/fwdmachine.rst | 88 ++++++++++++++++++-------
scapy/fwdmachine.py | 63 +++++++++---------
scapy/supersocket.py | 16 +++--
4 files changed, 180 insertions(+), 61 deletions(-)
create mode 100755 doc/scapy/_static/vethrelay.sh
diff --git a/doc/scapy/_static/vethrelay.sh b/doc/scapy/_static/vethrelay.sh
new file mode 100755
index 00000000000..0e62f7b4903
--- /dev/null
+++ b/doc/scapy/_static/vethrelay.sh
@@ -0,0 +1,74 @@
+#!/bin/bash
+
+# Setup iptables for IP relay by creating an interface configured
+# to be the destination of TPROXY rules.
+
+if [ "$EUID" -ne 0 ]
+ then echo "Please run as root"
+ exit
+fi
+
+if [ "$1" != "setup" ] && [ "$1" != "unsetup" ]; then
+ echo -e "Usage: ./vethrelay \n"
+ exit 1
+fi
+
+IFACE="vethrelay"
+IP="2.2.2.2"
+
+# Linux doc about TPROXY and example regarding this:
+# https://www.kernel.org/doc/Documentation/networking/tproxy.txt
+# https://powerdns.org/tproxydoc/tproxy.md.html
+
+function checkSetup() {
+ iptables -t mangle -n --list "DIVERT" >/dev/null 2>&1
+ return $?
+}
+
+if [ "$1" == "setup" ]; then
+ # Add "DIVERT" chain if it doesn't exist
+ checkSetup
+ if [ $? -eq 0 ]; then
+ echo "vethrelay already setup !"
+ exit 1
+ fi
+ # Create an interface tcpreplay dedicated to relay
+ ip link add dev $IFACE type dummy
+ sysctl net.ipv6.conf.$IFACE.disable_ipv6=1 >/dev/null
+ ip link set dev $IFACE up
+ ip addr add dev $IFACE $IP/32
+ # Create mangle "DIVERT" chain as an optimisation. -m socket matches
+ # packets from already established sockets. Those are marked as 1 then
+ # accepted directly.
+ iptables -t mangle -N DIVERT
+ iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
+ iptables -t mangle -A DIVERT -j MARK --set-mark 1
+ iptables -t mangle -A DIVERT -j ACCEPT
+ # Packets marked with 1 are routed through table 100 instead of the
+ # default routing table
+ ip rule add fwmark 1 lookup 100
+ # In routing table 100, all IPs are local to 'vethrelay'
+ ip route add local 0.0.0.0/0 dev $IFACE table 100
+ echo -e "\x1b[32mInterface $IFACE is now setup with IPv4: $IP !\x1b[0m\n"
+ echo -e "Add listening rules as follow:\n"
+ echo "# TPROXY incoming TCP packets on port 80 to $IFACE on port 8080"
+ echo "iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY --tproxy-mark 0x1/0x1 --on-port 8080 --on-ip $IP"
+ echo
+ echo "# Listen on wlp4s0 for incoming packets on port 80 (on the interface where it really comes from)"
+ echo "iptables -A INPUT -i wlp4s0 -p tcp --dport 80 -j ACCEPT"
+elif [ "$1" == "unsetup" ]; then
+ checkSetup
+ if [ $? -ne 0 ]; then
+ echo "vethrelay not setup !"
+ exit 1
+ fi
+ # Remove all setup rules
+ sudo ip rule del fwmark 1 lookup 100
+ sudo ip route del local 0.0.0.0/0 dev $IFACE table 100
+ sudo iptables -t mangle -D DIVERT -j ACCEPT
+ sudo iptables -t mangle -D DIVERT -j MARK --set-mark 1
+ sudo iptables -t mangle -D PREROUTING -p tcp -m socket -j DIVERT
+ sudo iptables -t mangle -X DIVERT
+ sudo ip link del dev $IFACE
+ echo -e "\x1b[32mInterface $IFACE unsetup !\x1b[0m"
+fi
diff --git a/doc/scapy/advanced_usage/fwdmachine.rst b/doc/scapy/advanced_usage/fwdmachine.rst
index ed26929a729..d51594c1b0e 100644
--- a/doc/scapy/advanced_usage/fwdmachine.rst
+++ b/doc/scapy/advanced_usage/fwdmachine.rst
@@ -2,19 +2,24 @@
Forwarding Machine
******************
-Scapy's ``ForwardingMachine`` is a utility that allows to create server that forwards packets to another server, with the ability
-to modify them on-the-fly. This is similar to a "proxy", but works with any protocols over IP/IPv6. The ``ForwardingMachine`` was initially designed to be used with TPROXY,
+Scapy's ``ForwardMachine`` is a utility that allows to create server that forwards packets to another server, with the ability
+to modify them on-the-fly. This is similar to a "proxy", but works with any protocols over IP/IPv6. The ``ForwardMachine`` was initially designed to be used with TPROXY,
a linux feature that allows to bind a socket that received *packets to any IP destination* (in which case it properly forwards the packet to the initially
intended destination), but it also work as a standalone server.
-A ``ForwardingMachine`` is expected to be used over a normal Python socket, of any kind, and needs to extended with two
+A ``ForwardMachine`` is expected to be used over a normal Python socket, of any kind, and needs to extended with two
functions: ``xfrmcs`` and ``xfrmsc``. The first one is called whenever data is received from the client side (client-to-server), the other when the data
is received from the server.
+``ForwardMachine`` can be used in two modes:
+
+- **TPROXY**
+- **SERVER**, in which case a normal socket is bound. Think of it as a glorified socat
+
Basic usage
___________
-Here's an example of a ``ForwardingMachine`` over TPROXY that does nothing. Packets for all destinations are handled, and forwarded to their
+Here's an example of a ``ForwardMachine`` over TPROXY that does nothing. Packets for all destinations are handled, and forwarded to their
initial destinations afterwards. More details on how to setup TPROXY are provided below.
.. code:: python
@@ -22,7 +27,7 @@ initial destinations afterwards. More details on how to setup TPROXY are provide
from scapy.fwdmachine import ForwardMachine
from scapy.layers.http import HTTP
- class HTTPEdit(ForwardMachine):
+ class NOPFwdMachine(ForwardMachine):
def xfrmcs(self, pkt, ctx):
pkt.show() # we print the client->server packets
raise self.FORWARD()
@@ -32,13 +37,13 @@ initial destinations afterwards. More details on how to setup TPROXY are provide
raise self.FORWARD()
# Run it
- HTTPEdit(
+ NOPFwdMachine(
mode=ForwardMachine.MODE.TPROXY,
port=80,
cls=HTTP, # we specify the class of the payload we are receiving
).run()
-The callback classes use **Operations** to tell the ``ForwardingMachine`` what to do with the incoming data.
+The callback classes use **Operations** to tell the ``ForwardMachine`` what to do with the incoming data.
.. figure:: ../graphics/fwdmachine.svg
:align: center
@@ -56,32 +61,34 @@ There are currently 5 operations available:
The ``ctx`` attribute in the callbacks contains context relative to the current client. It can also be use to
store additional data specific to the session.
+If we were to use this machine in SERVER mode, we would call it like:
+
+.. code:: python
+
+ NOPFwdMachine(
+ mode=ForwardMachine.MODE.SERVER,
+ port=12345,
+ bind_address="0.0.0.0", # the address we bind on
+ remote_address="192.168.0.1", # the server to redirect this to by default
+ cls=conf.raw_layer, # Default Raw layer: we don't know the type of data
+ ).run()
+
TLS support
___________
-``ForwardingMachine`` has support for TLS through the ``ssl=True`` argument. When TLS is enabled, the SNI (Server Name Indication) is
+``ForwardMachine`` has support for TLS through the ``ssl=True`` argument. When TLS is enabled, the SNI (Server Name Indication) is
properly forwarded to the remote peer, and can be accessed through the ``ctx.tls_sni_name`` attribute in the callbacks.
-**By default, a ForwardingMachine generates self-signed certificates** that copy the attributes from the certificate of the remote
+**By default, a ForwardMachine generates self-signed certificates** that copy the attributes from the certificate of the remote
server. This behavior can be changed by specifying a certificate (which will be served by the TLS stack).
-.. code:: python
+We can run the same ForwardMachine as from the previous example, this time with self-signed TLS.
- from scapy.fwdmachine import ForwardMachine
- from scapy.layers.http import HTTP
-
- class HTTPSDump(ForwardMachine):
- def xfrmcs(self, pkt, ctx):
- pkt.show() # we print the client->server packets
- raise self.FORWARD()
-
- def xfrmsc(self, pkt, ctx):
- pkt.show() # we print the server->client packets
- raise self.FORWARD()
+.. code:: python
# Run it
- HTTPSDump(
- mode=ForwardMachine.MODE.TPROXY,
+ NOPFwdMachine(
+ mode=ForwardMachine.MODE.SERVER,
port=443,
cls=HTTP,
ssl=True,
@@ -90,4 +97,37 @@ server. This behavior can be changed by specifying a certificate (which will be
Configuring TPROXY
__________________
-For ease of use, a script
\ No newline at end of file
+TPROXY is a special socket mode that allows to bind a socket that listens for traffic that isn't directed at a local address. This is typically used by "transparent TLS proxies" to achieve their functionality, and is expected to be setup on a linux router.
+
+The ``ForwardingMachine`` supports TPROXY, which allows to intercept and modify all the traffic by many clients to many destinations, for instance on a specific port. This is much more versatile that a classic bind + socket, which would typically forward multiple clients to a single destination.
+
+Here are the steps:
+
+- Setup an interface that one can redirect traffic to, and that has TPROXY support.
+- Bind the ``ForwardingMachine`` on that interface.
+- Redirect some traffic to that interface, using ``iptables`` or ``nftables``, based on some arbitrary criteria.
+
+For ease of use, a script ``vethrelay.sh`` is provided to setup a veth (virtual ethernet) interface that can be used to bind the ``ForwardingMachine`` on. This script is available at https://github.com/secdev/scapy/blob/master/doc/scapy/_static/vethrelay.sh
+
+.. code:: bash
+
+ ./vethrelay.sh setup
+ Interface vethrelay is now setup with IPv4: 2.2.2.2 !
+
+ Add listening rules as follow:
+
+ # TPROXY incoming TCP packets on port 80 to vethrelay on port 8080
+ iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY --tproxy-mark 0x1/0x1 --on-port 8080 --on-ip 2.2.2.2
+
+ # Listen on wlp4s0 for incoming packets on port 80 (on the interface where it really comes from)
+ iptables -A INPUT -i wlp4s0 -p tcp --dport 80 -j ACCEPT
+
+As the instructions say, to have traffic to anything on the port 80 go through the ``ForwardingMachine``, one can run the commands listed above assuming that the machine is started as such:
+
+.. code:: python
+
+ NOPFwdMachine(
+ mode=ForwardMachine.MODE.TPROXY,
+ port=8080,
+ cls=HTTP,
+ ).run()
diff --git a/scapy/fwdmachine.py b/scapy/fwdmachine.py
index 134b8d13c01..cc74db06acd 100644
--- a/scapy/fwdmachine.py
+++ b/scapy/fwdmachine.py
@@ -23,8 +23,8 @@
from scapy.config import conf
from scapy.data import MTU
from scapy.packet import Packet
-from scapy.supersocket import StreamSocket, SSLStreamSocket
-from scapy.themes import DefaultTheme, ColorOnBlackTheme
+from scapy.supersocket import StreamSocket, StreamSocketPeekless
+from scapy.themes import DefaultTheme
from scapy.utils import get_temp_file
from scapy.volatile import RandInt
@@ -57,7 +57,7 @@ class ForwardMachine:
two modes:
- SERVER: the server binds a port on its local IP and forwards packets to a
- `remote_ip`.
+ `remote_address`.
- TPROXY: the server binds can intercept packets to any IP destination, provided
that they are routed through the local server, and some tweaking of the OS
routes;
@@ -75,11 +75,11 @@ class ForwardMachine:
:param cls: the scapy class to parse on that port
:param af: the address family to use (default AF_INET)
:param proto: the proto to use (default SOCK_STREAM)
- :param remote_ip: the IP to use in SERVER mode, or by default in TPROXY when the destination
+ :param remote_address: the IP to use in SERVER mode, or by default in TPROXY when the destination
is the local IP.
:param remote_af: (optional) if provided, use a different address family to connect
to the remote host.
- :param bind_ip: the IP to bind locally. "0.0.0.0" by default in SERVER mode, but "2.2.2.2" by default
+ :param bind_address: the IP to bind locally. "0.0.0.0" by default in SERVER mode, but "2.2.2.2" by default
in TPROXY (if you are using the provided 'vethrelay.sh' script).
:param tls: enable TLS (in both the server and client)
:param crtfile: (optional) if provided, uses a certificate instead of self signed ones.
@@ -107,9 +107,9 @@ def __init__(
cls: Type[Packet],
af: socket.AddressFamily = socket.AF_INET,
proto: socket.SocketKind = socket.SOCK_STREAM,
- remote_ip: str = None,
+ remote_address: str = None,
remote_af: Optional[socket.AddressFamily] = None,
- bind_ip: str = None,
+ bind_address: str = None,
tls: bool = False,
crtfile: Optional[str] = None,
keyfile: Optional[str] = None,
@@ -121,27 +121,30 @@ def __init__(
self.port = port
self.cls = cls
self.af = af
- self.remote_af = remote_af or af
+ self.remote_af = remote_af if remote_af is not None else af
self.proto = proto
self.tls = tls
self.crtfile = crtfile
self.keyfile = keyfile
self.timeout = timeout
self.MTU = MTU
- self.remote_ip = remote_ip
- if self.tls:
- self.sockcls = SSLStreamSocket
+ self.remote_address = remote_address
+ if self.tls or self.af == 40: # TLS or VSOCK
+ self.sockcls = StreamSocketPeekless
else:
self.sockcls = StreamSocket
- # Chose 'bind_ip' depending on the mode
- if self.mode == ForwardMachine.MODE.SERVER:
- self.bind_ip = "0.0.0.0"
- elif self.mode == ForwardMachine.MODE.TPROXY:
- self.bind_ip = "2.2.2.2"
- else:
- raise ValueError("Unknown mode :/")
+ # Chose 'bind_address' depending on the mode
+ self.bind_address = bind_address
+ if self.bind_address is None:
+ if self.mode == ForwardMachine.MODE.SERVER:
+ self.bind_address = "0.0.0.0"
+ elif self.mode == ForwardMachine.MODE.TPROXY:
+ self.bind_address = "2.2.2.2"
+ else:
+ raise ValueError("Unknown mode :/")
red = lambda z: functools.reduce(lambda x, y: x + y, z)
- # Session settings
+ # Utils
+ self.ct = DefaultTheme()
self.local_ips = red(red(list(x.ips.values())) for x in conf.ifaces.values())
self.cache = {}
super(ForwardMachine, self).__init__(**kwargs)
@@ -154,23 +157,23 @@ def run(self):
self.ssock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if self.mode == ForwardMachine.MODE.TPROXY:
self.ssock.setsockopt(socket.SOL_IP, socket.IP_TRANSPARENT, 1) # TPROXY !
- self.ssock.bind((self.bind_ip, self.port))
+ self.ssock.bind((self.bind_address, self.port))
self.ssock.listen(5)
- print(ct.green("Relay server waiting on port %s" % self.port))
+ print(self.ct.green("Relay server waiting on port %s" % self.port))
while True:
conn, addr = self.ssock.accept()
# Calc dest
dest = conn.getsockname()
- if dest[0] in self.local_ips and self.remote_ip:
- dest = (self.remote_ip,) + dest[1:]
- print(ct.green("%s -> %s connected !" % (repr(addr), repr(dest))))
+ if self.mode == ForwardMachine.MODE.SERVER or (dest[0] in self.local_ips and self.remote_address):
+ dest = (self.remote_address,) + dest[1:]
+ print(self.ct.green("%s -> %s connected !" % (repr(addr), repr(dest))))
try:
threading.Thread(
target=self.handler,
args=(conn, addr, dest),
).start()
except Exception as ex:
- print(ct.red("%s errored !" % repr(addr)))
+ print(self.ct.red("%s errored !" % repr(addr)))
conn.close()
pass
@@ -262,7 +265,7 @@ def _getpeersock(self, dest, server_hostname=None):
if ndest != dest:
print("C: %s redirected to %s" % (repr(dest), repr(ndest)))
dest = ndest
- s.connect(dest) # we connect to the real destination server
+ s.connect(dest)
return s
def gen_alike_chain(self, certs, privkey):
@@ -397,7 +400,7 @@ def cb_sni(sock, server_name, _):
try:
sock = sslcontext.wrap_socket(sock, server_side=True)
except Exception as ex:
- print(ct.red("%s errored in SSL: %s" % (repr(addr), str(ex))))
+ print(self.ct.red("%s errored in SSL: %s" % (repr(addr), str(ex))))
sock.close()
return
ss = _clisock[0]
@@ -429,7 +432,7 @@ def cb_sni(sock, server_name, _):
try:
func(data, ctx)
# If this doesn't raise, it's a user error.
- print(ct.red("%s ERROR: you must always raise in %s !" % func))
+ print(self.ct.red("%s ERROR: you must always raise in %s !" % func))
return
except self.REDIRECT_TO as ex:
# Replace the peer socket with a new socket
@@ -468,7 +471,7 @@ def cb_sni(sock, server_name, _):
except Exception as ex:
# Processing failed. forward to not break anything
print(
- ct.orange(
+ self.ct.orange(
"Exception happend in handling client %s ! (forwarding.)"
% repr(addr)
)
@@ -477,6 +480,6 @@ def cb_sni(sock, server_name, _):
othersock.send(data)
self.print_reply(self.FORWARD, cs, data, None)
except RuntimeError:
- print(ct.red("%s DISCONNECTED !" % repr(addr)))
+ print(self.ct.red("%s DISCONNECTED !" % repr(addr)))
sock.close()
ss.close()
diff --git a/scapy/supersocket.py b/scapy/supersocket.py
index 9744fd841bf..a6009261616 100644
--- a/scapy/supersocket.py
+++ b/scapy/supersocket.py
@@ -502,16 +502,14 @@ def recv(self, x=None, **kwargs):
return pkt
-class SSLStreamSocket(StreamSocket):
- desc = "similar usage than StreamSocket but specialized for handling SSL-wrapped sockets" # noqa: E501
-
- # Basically StreamSocket but we can't PEEK
+class StreamSocketPeekless(StreamSocket):
+ desc = "StreamSocket that doesn't use MSG_PEEK"
def __init__(self, sock, basecls=None):
# type: (socket.socket, Optional[Type[Packet]]) -> None
from scapy.sessions import TCPSession
self.sess = TCPSession(app=True)
- super(SSLStreamSocket, self).__init__(sock, basecls)
+ super(StreamSocketPeekless, self).__init__(sock, basecls)
# 65535, the default value of x is the maximum length of a TLS record
def recv(self, x=None, **kwargs):
@@ -540,11 +538,15 @@ def select(sockets, remain=None):
queued = [
x
for x in sockets
- if isinstance(x, SSLStreamSocket) and x.sess.data
+ if isinstance(x, StreamSocketPeekless) and x.sess.data
]
if queued:
return queued # type: ignore
- return super(SSLStreamSocket, SSLStreamSocket).select(sockets, remain=remain)
+ return super(StreamSocketPeekless, StreamSocketPeekless).select(sockets, remain=remain)
+
+
+# Old name: SSLStreamSocket
+SSLStreamSocket = StreamSocketPeekless
class L2ListenTcpdump(SuperSocket):
From 878b6bede06d62e7e9780961871ba62a36e71083 Mon Sep 17 00:00:00 2001
From: gpotter2 <10530980+gpotter2@users.noreply.github.com>
Date: Thu, 22 May 2025 00:03:49 +0200
Subject: [PATCH 4/9] Add debug_strfixedlenfield
---
scapy/config.py | 2 ++
scapy/fields.py | 4 ++--
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/scapy/config.py b/scapy/config.py
index a692a6d4efe..1a70b7cf440 100755
--- a/scapy/config.py
+++ b/scapy/config.py
@@ -1129,6 +1129,8 @@ class Conf(ConfClass):
#: Windows SSPs for sniffing. This is used with
#: dcerpc_session_enable
winssps_passive = []
+ #: Disables auto-stripping of StrFixedLenField for debugging purposes
+ debug_strfixedlenfield = False
def __getattribute__(self, attr):
# type: (str) -> Any
diff --git a/scapy/fields.py b/scapy/fields.py
index 56e280b25c5..46064b726e2 100644
--- a/scapy/fields.py
+++ b/scapy/fields.py
@@ -1865,7 +1865,7 @@ def __init__(
name, # type: str
default, # type: Optional[bytes]
length=None, # type: Optional[int]
- length_from=None, # type: Optional[Callable[[Packet], int]] # noqa: E501
+ length_from=None, # type: Optional[Callable[[Packet], int]]
):
# type: (...) -> None
super(StrFixedLenField, self).__init__(name, default)
@@ -1879,7 +1879,7 @@ def i2repr(self,
v, # type: bytes
):
# type: (...) -> str
- if isinstance(v, bytes):
+ if isinstance(v, bytes) and not conf.debug_strfixedlenfield:
v = v.rstrip(b"\0")
return super(StrFixedLenField, self).i2repr(pkt, v)
From 40da1784f908ffd1ab2a4a7332ac0cb8d015a7c4 Mon Sep 17 00:00:00 2001
From: gpotter2 <10530980+gpotter2@users.noreply.github.com>
Date: Tue, 10 Jun 2025 01:14:03 +0200
Subject: [PATCH 5/9] Cleanup Cert/Key handling a LOT
---
scapy/layers/tls/cert.py | 207 +++++++++++++++++++--------------
test/scapy/layers/tls/cert.uts | 37 +++++-
2 files changed, 152 insertions(+), 92 deletions(-)
diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py
index 58be5ce6438..dd278df545f 100644
--- a/scapy/layers/tls/cert.py
+++ b/scapy/layers/tls/cert.py
@@ -10,11 +10,6 @@
Supports both RSA and ECDSA objects.
The classes below are wrappers for the ASN.1 objects defined in x509.py.
-By collecting their attributes, we bypass the ASN.1 structure, hence
-there is no direct method for exporting a new full DER-encoded version
-of a Cert instance after its serial has been modified (for example).
-If you need to modify an import, just use the corresponding ASN1_Packet.
-
For instance, here is what you could do in order to modify the serial of
'cert' and then resign it with whatever 'key'::
@@ -134,16 +129,9 @@ def split_pem(s):
class _PKIObj(object):
- def __init__(self, frmt, der, pem):
- # Note that changing attributes of the _PKIObj does not update these
- # values (e.g. modifying k.modulus does not change k.der).
- # XXX use __setattr__ for this
+ def __init__(self, frmt, der):
self.frmt = frmt
- self.der = der
- self.pem = pem
-
- def __str__(self):
- return self.der
+ self._der = der
class _PKIObjMaker(type):
@@ -181,15 +169,10 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None):
else:
frmt = "DER"
der = _raw
- pem = ""
- if pem_marker is not None:
- pem = der2pem(_raw, pem_marker)
- # type identification may be needed for pem_marker
- # in such case, the pem attribute has to be updated
except Exception:
raise Exception(error_msg)
- p = _PKIObj(frmt, der, pem)
+ p = _PKIObj(frmt, der)
return p
@@ -207,7 +190,15 @@ class _PubKeyFactory(_PKIObjMaker):
It casts the appropriate class on the fly, then fills in
the appropriate attributes with import_from_asn1pkt() submethod.
"""
- def __call__(cls, key_path=None):
+ def __call__(cls, key_path=None, cryptography_obj=None):
+ # This allows to import cryptography objects directly
+ if cryptography_obj is not None:
+ obj = type.__call__(cls)
+ obj.__class__ = cls
+ obj.frmt = "original"
+ obj.marker = "PUBLIC KEY"
+ obj.pubkey = cryptography_obj
+ return obj
if key_path is None:
obj = type.__call__(cls)
@@ -233,41 +224,38 @@ def __call__(cls, key_path=None):
# _an EdDSAPublicKey.
obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE)
try:
- spki = X509_SubjectPublicKeyInfo(obj.der)
+ spki = X509_SubjectPublicKeyInfo(obj._der)
pubkey = spki.subjectPublicKey
if isinstance(pubkey, RSAPublicKey):
obj.__class__ = PubKeyRSA
obj.import_from_asn1pkt(pubkey)
elif isinstance(pubkey, ECDSAPublicKey):
obj.__class__ = PubKeyECDSA
- obj.import_from_der(obj.der)
+ obj.import_from_der(obj._der)
elif isinstance(pubkey, EdDSAPublicKey):
obj.__class__ = PubKeyEdDSA
- obj.import_from_der(obj.der)
+ obj.import_from_der(obj._der)
else:
raise
- marker = b"PUBLIC KEY"
+ obj.marker = "PUBLIC KEY"
except Exception:
try:
- pubkey = RSAPublicKey(obj.der)
+ pubkey = RSAPublicKey(obj._der)
obj.__class__ = PubKeyRSA
obj.import_from_asn1pkt(pubkey)
- marker = b"RSA PUBLIC KEY"
+ obj.marker = "RSA PUBLIC KEY"
except Exception:
# We cannot import an ECDSA public key without curve knowledge
if conf.debug_dissector:
raise
raise Exception("Unable to import public key")
-
- if obj.frmt == "DER":
- obj.pem = der2pem(obj.der, marker)
return obj
class PubKey(metaclass=_PubKeyFactory):
"""
- Parent class for both PubKeyRSA and PubKeyECDSA.
- Provides a common verifyCert() method.
+ Parent class for PubKeyRSA, PubKeyECDSA and PubKeyEdDSA.
+ Provides common verifyCert() and export() methods.
"""
def verifyCert(self, cert):
@@ -278,6 +266,27 @@ def verifyCert(self, cert):
sigVal = raw(cert.signatureValue)
return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs')
+ @property
+ def pem(self):
+ return der2pem(self.der, self.marker)
+
+ @property
+ def der(self):
+ return self.pubkey.public_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+
+ def export(self, filename, fmt="DER"):
+ """
+ Export public key in 'fmt' format (DER or PEM) to file 'filename'
+ """
+ with open(filename, "wb") as f:
+ if fmt == "DER":
+ f.write(self.der)
+ elif fmt == "PEM":
+ f.write(self.pem)
+
class PubKeyRSA(PubKey, _EncryptAndVerifyRSA):
"""
@@ -308,6 +317,9 @@ def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None):
warning("modulus and modulusLen do not match!")
pubNum = rsa.RSAPublicNumbers(n=modulus, e=pubExp)
self.pubkey = pubNum.public_key(default_backend())
+
+ self.marker = "PUBLIC KEY"
+
# Lines below are only useful for the legacy part of pkcs1.py
pubNum = self.pubkey.public_numbers()
self._modulusLen = real_modulusLen
@@ -323,10 +335,6 @@ def import_from_tuple(self, tup):
if isinstance(e, bytes):
e = pkcs_os2ip(e)
self.fill_and_store(modulus=m, pubExp=e)
- self.pem = self.pubkey.public_bytes(
- encoding=serialization.Encoding.PEM,
- format=serialization.PublicFormat.SubjectPublicKeyInfo)
- self.der = pem2der(self.pem)
def import_from_asn1pkt(self, pubkey):
modulus = pubkey.modulus.val
@@ -433,47 +441,38 @@ def __call__(cls, key_path=None):
return obj
obj = _PKIObjMaker.__call__(cls, key_path, _MAX_KEY_SIZE)
- multiPEM = False
try:
- privkey = RSAPrivateKey_OpenSSL(obj.der)
+ privkey = RSAPrivateKey_OpenSSL(obj._der)
privkey = privkey.privateKey
obj.__class__ = PrivKeyRSA
- marker = b"PRIVATE KEY"
+ obj.marker = "PRIVATE KEY"
except Exception:
try:
- privkey = ECDSAPrivateKey_OpenSSL(obj.der)
+ privkey = ECDSAPrivateKey_OpenSSL(obj._der)
privkey = privkey.privateKey
obj.__class__ = PrivKeyECDSA
- marker = b"EC PRIVATE KEY"
- multiPEM = True
+ obj.marker = "EC PRIVATE KEY"
except Exception:
try:
- privkey = RSAPrivateKey(obj.der)
+ privkey = RSAPrivateKey(obj._der)
obj.__class__ = PrivKeyRSA
- marker = b"RSA PRIVATE KEY"
+ obj.marker = "RSA PRIVATE KEY"
except Exception:
try:
- privkey = ECDSAPrivateKey(obj.der)
+ privkey = ECDSAPrivateKey(obj._der)
obj.__class__ = PrivKeyECDSA
- marker = b"EC PRIVATE KEY"
+ obj.marker = "EC PRIVATE KEY"
except Exception:
try:
- privkey = EdDSAPrivateKey(obj.der)
+ privkey = EdDSAPrivateKey(obj._der)
obj.__class__ = PrivKeyEdDSA
- marker = b"PRIVATE KEY"
+ obj.marker = "PRIVATE KEY"
except Exception:
raise Exception("Unable to import private key")
try:
obj.import_from_asn1pkt(privkey)
except ImportError:
pass
-
- if obj.frmt == "DER":
- if multiPEM:
- # this does not restore the EC PARAMETERS header
- obj.pem = der2pem(raw(privkey), marker)
- else:
- obj.pem = der2pem(obj.der, marker)
return obj
@@ -486,8 +485,9 @@ def __bytes__(self):
class PrivKey(metaclass=_PrivKeyFactory):
"""
- Parent class for both PrivKeyRSA and PrivKeyECDSA.
- Provides common signTBSCert() and resignCert() methods.
+ Parent class for PrivKeyRSA, PrivKeyECDSA and PrivKeyEdDSA.
+ Provides common signTBSCert(), resignCert(), verifyCert()
+ and export() methods.
"""
def signTBSCert(self, tbsCert, h="sha256"):
@@ -525,8 +525,30 @@ def verifyCert(self, cert):
sigVal = raw(cert.signatureValue)
return self.verify(raw(tbsCert), sigVal, h=h, t='pkcs')
+ @property
+ def pem(self):
+ return der2pem(self.der, self.marker)
+
+ @property
+ def der(self):
+ return self.key.private_bytes(
+ encoding=serialization.Encoding.DER,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption()
+ )
+
+ def export(self, filename, fmt="DER"):
+ """
+ Export private key in 'fmt' format (DER or PEM) to file 'filename'
+ """
+ with open(filename, "wb") as f:
+ if fmt == "DER":
+ f.write(self.der)
+ elif fmt == "PEM":
+ f.write(self.pem)
+
-class PrivKeyRSA(PrivKey, _EncryptAndVerifyRSA, _DecryptAndSignRSA):
+class PrivKeyRSA(PrivKey, _DecryptAndSignRSA):
"""
Wrapper for RSA keys based on _DecryptAndSignRSA from crypto/pkcs1.py
Use the 'key' attribute to access original object.
@@ -554,7 +576,7 @@ def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None,
key_size=real_modulusLen,
backend=default_backend(),
)
- self.pubkey = self.key.public_key()
+ pubkey = self.key.public_key()
else:
real_modulusLen = len(binrepr(modulus))
if modulusLen and real_modulusLen != modulusLen:
@@ -565,14 +587,18 @@ def fill_and_store(self, modulus=None, modulusLen=None, pubExp=None,
iqmp=coefficient, d=privExp,
public_numbers=pubNum)
self.key = privNum.private_key(default_backend())
- self.pubkey = self.key.public_key()
+ pubkey = self.key.public_key()
+
+ self.marker = "PRIVATE KEY"
# Lines below are only useful for the legacy part of pkcs1.py
- pubNum = self.pubkey.public_numbers()
+ pubNum = pubkey.public_numbers()
self._modulusLen = real_modulusLen
self._modulus = pubNum.n
self._pubExp = pubNum.e
+ self.pubkey = PubKeyRSA((pubNum.e, pubNum.n, real_modulusLen))
+
def import_from_asn1pkt(self, privkey):
modulus = privkey.modulus.val
pubExp = privkey.publicExponent.val
@@ -588,9 +614,14 @@ def import_from_asn1pkt(self, privkey):
coefficient=coefficient)
def verify(self, msg, sig, t="pkcs", h="sha256", mgf=None, L=None):
- # Let's copy this from PubKeyRSA instead of adding another baseclass :)
- return _EncryptAndVerifyRSA.verify(
- self, msg, sig, t=t, h=h, mgf=mgf, L=L)
+ return self.pubkey.verify(
+ msg=msg,
+ sig=sig,
+ t=t,
+ h=h,
+ mgf=mgf,
+ L=L,
+ )
def sign(self, data, t="pkcs", h="sha256", mgf=None, L=None):
return _DecryptAndSignRSA.sign(self, data, t=t, h=h, mgf=mgf, L=L)
@@ -605,22 +636,19 @@ class PrivKeyECDSA(PrivKey):
def fill_and_store(self, curve=None):
curve = curve or ec.SECP256R1
self.key = ec.generate_private_key(curve(), default_backend())
- self.pubkey = self.key.public_key()
+ self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key())
+ self.marker = "EC PRIVATE KEY"
@crypto_validator
def import_from_asn1pkt(self, privkey):
self.key = serialization.load_der_private_key(raw(privkey), None,
backend=default_backend()) # noqa: E501
- self.pubkey = self.key.public_key()
+ self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key())
+ self.marker = "EC PRIVATE KEY"
@crypto_validator
def verify(self, msg, sig, h="sha256", **kwargs):
- # 'sig' should be a DER-encoded signature, as per RFC 3279
- try:
- self.pubkey.verify(sig, msg, ec.ECDSA(_get_hash(h)))
- return True
- except InvalidSignature:
- return False
+ return self.pubkey.verify(msg=msg, sig=sig, h=h, **kwargs)
@crypto_validator
def sign(self, data, h="sha256", **kwargs):
@@ -636,22 +664,19 @@ class PrivKeyEdDSA(PrivKey):
def fill_and_store(self, curve=None):
curve = curve or x25519.X25519PrivateKey
self.key = curve.generate()
- self.pubkey = self.key.public_key()
+ self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key())
+ self.marker = "PRIVATE KEY"
@crypto_validator
def import_from_asn1pkt(self, privkey):
self.key = serialization.load_der_private_key(raw(privkey), None,
backend=default_backend()) # noqa: E501
- self.pubkey = self.key.public_key()
+ self.pubkey = PubKeyECDSA(cryptography_obj=self.key.public_key())
+ self.marker = "PRIVATE KEY"
@crypto_validator
def verify(self, msg, sig, **kwargs):
- # 'sig' should be a DER-encoded signature, as per RFC 3279
- try:
- self.pubkey.verify(sig, msg)
- return True
- except InvalidSignature:
- return False
+ return self.pubkey.verify(msg=msg, sig=sig, **kwargs)
@crypto_validator
def sign(self, data, **kwargs):
@@ -671,8 +696,9 @@ def __call__(cls, cert_path):
obj = _PKIObjMaker.__call__(cls, cert_path,
_MAX_CERT_SIZE, "CERTIFICATE")
obj.__class__ = Cert
+ obj.marker = "CERTIFICATE"
try:
- cert = X509_Cert(obj.der)
+ cert = X509_Cert(obj._der)
except Exception:
if conf.debug_dissector:
raise
@@ -783,11 +809,12 @@ def setSubjectPublicKeyFromPrivateKey(self, key):
the provided key.
"""
if isinstance(key, (PubKey, PrivKey)):
+ if isinstance(key, PrivKey):
+ pubkey = key.pubkey
+ else:
+ pubkey = key
self.tbsCertificate.subjectPublicKeyInfo = X509_SubjectPublicKeyInfo(
- key.pubkey.public_bytes(
- encoding=serialization.Encoding.DER,
- format=serialization.PublicFormat.SubjectPublicKeyInfo,
- )
+ pubkey.der
)
else:
raise ValueError("Unknown type 'key', should be PubKey or PrivKey")
@@ -854,6 +881,14 @@ def isRevoked(self, crl_list):
return self.serial in (x[0] for x in c.revoked_cert_serials)
return False
+ @property
+ def pem(self):
+ return der2pem(self.der, self.marker)
+
+ @property
+ def der(self):
+ return bytes(self.x509Cert)
+
def export(self, filename, fmt="DER"):
"""
Export certificate in 'fmt' format (DER or PEM) to file 'filename'
@@ -887,7 +922,7 @@ def __call__(cls, cert_path):
obj = _PKIObjMaker.__call__(cls, cert_path, _MAX_CRL_SIZE, "X509 CRL")
obj.__class__ = CRL
try:
- crl = X509_CRL(obj.der)
+ crl = X509_CRL(obj._der)
except Exception:
raise Exception("Unable to import CRL")
obj.import_from_asn1pkt(crl)
diff --git a/test/scapy/layers/tls/cert.uts b/test/scapy/layers/tls/cert.uts
index ceeeeb719bf..4ef835a140e 100644
--- a/test/scapy/layers/tls/cert.uts
+++ b/test/scapy/layers/tls/cert.uts
@@ -108,7 +108,7 @@ W3a3ICJ/ZYLvkgb4IBEteOwWpp37fX57vzhW8EmUV2UX7ve1uNRI
-----END RSA PRIVATE KEY-----
""")
x_privNum = x.key.private_numbers()
-x_pubNum = x.pubkey.public_numbers()
+x_pubNum = x.pubkey.pubkey.public_numbers()
type(x) is PrivKeyRSA
= PrivKey class : Checking public attributes
@@ -150,15 +150,16 @@ assert not a.verify(b"Hello", data)
= PubKeyECDSA verify
~ crypto_advanced
-b = PubKeyECDSA()
-b.pubkey = a.pubkey
+assert isinstance(a.pubkey, PubKeyECDSA)
+
+b = PubKeyECDSA(cryptography_obj=a.pubkey.pubkey)
assert b.verify(msg, data)
assert not b.verify(b"Hello", data)
= PrivKey class : Importing DER-encoded RSA private key
a = PrivKeyRSA(b'0\x82\x04\xa3\x02\x01\x00\x02\x82\x01\x01\x00\x98Wj?\xe9\xd3\x11\x9b\xa4KIK?\xec\xa3\xd6\x03H\x9a\xc1\x08\x7f\xb3\xf6\xc9$\xee\x9d\xc7\x98\xc7\t&\xe1Q9A\x17\x83m\xbd\x8b\xe7\xf1\xcb\x03\xdaO\x98\x87\x90-*\xbf\x06\x03\xb5\x99\xe3\x9cl\xe4\x89\xd9\x85GCo\x0cC\x9e\xbe\xf0*\xdb\xea}\xbc\x8b\'\x17\xe2\x1at\x1fp1D\x08\xe1\xd1\xe7W\xfa\xad\xf2\x8a[\xd8\'\x85\xbd\xfc\xd9\xc8o\xfc\x00g\x04\xb4\xa0\x98\x9f\xfe\xd4\xe4T^\xfb\x1f&\xc0|\x97^\xe4J\x9b\xa7\xe6\xc2(\x8b\xccZv\xa6n\x1fCEL\xa3\xac\x10Y\xa3\x97@\xd6\x8d\xf6\xce\x9b\x85\x06\xb2]#\xc7fR\x9c=\x82\xd7\xf4\x17@Z\xf2Q\x99\x9b\xc5*sA\xb2]\xe5\xce%A6\xbb\xb0\xa22\xed\xcc\xef\xb0L\xe9\x92\xcbM\xca0\xe7\xe6\xd0"i&L\xbdR\x1a\x1c\xf0~)\xcc\x13W\xba\xa7q\xe6\xff\xfaC\x8e\xe2o\x15\xa66\xdaM9.\x02\xee\xca\xa79\xf6\xf1b\x07t\xe8\x95\xdc\xfc\xf8\x06\xcc6;\xf3\x03\x02\x03\x01\x00\x01\x02\x82\x01\x00}\xcax\x96K\xda\x18H\xffQ\x974\xc6\x94\xfc\xf7\xc3\x80Y \x99\x86\xf10\x0f\t*\xeb\'\x9b\xf4\x85\x8f\x100\x04i\xc6#\xa5#\x05zA\x82\x94,\xd8\xda\xa6\xdd\x9b\x1e\x17\xdb\xbc\x86`\x8a\xbch\x82\x11}\x86z\xc0\xa8\xdad\x9f\x99$1\x0f\xa4A\xac\xc4\xeeC\xdfT^\x9cs\x04\x8b\x1c\x16s?f\xbb<\x94\xf0@Dl\xe6\x17i\xc8\xde\xa3\xf1^\xd7\xb1\xe0\x00W\xe6\x8d\x02w\x83_fVc\xa6?z\xb2E(;\xcf\x9bwr88\xf5\x05`QE\xba\xff5.\x84\x90\xe8w\xb0\xd1\xaf1\xb4\x95\xed*@\xe9\xf6\x1dcLa\x949\xcd|\xf5`\x026\x80K\x1fC\x06yZ\x12Q\xf75\xfb\xe4\\\x0bw2\xa5F||\xd3\xf58\xee\x86\x05\xfb\xf4\xe9\xfeh\\D\xb8e\xbcFb\x96\xbe\x9c\'\xcd\xc1\xbe\x95\xda\x05\x9b\x92\xf6\x98\x9b\xb6\x85\x9fB\x96\x18\xd6WKS\xf79\xc5\xaf\xe8\x85\x9d:h6\x08\x8e\xcc0\x12\xc9\xd0\xbc\x96T!\x02\x81\x81\x00\xc8\xc2X\xc3_y\x9a\xf1\xe5\xf1\\\xb1\'q\xe4\xc5\xe4\x1aj\x8c\xd6/\x8a\xa15\r\nk\x0f\xa6\xcc?\xa3a\xd9\x1b\x1d\xbd\xda\x05\x88!\xb2\x01\x03f9\xf29V\xe5\x0b\xb2>\xe2\xc9\xaf\x01\x92KX\x96\x9f\xea\xc6y\x1c\x0c7\xceh\xf5\x89\xb0\xa4_\x1e\xddz\x84\\\xfd\x05\\:)%\xdd\xd1?\xcac\xb4(bM\x88l\xc9fl[E-\xe1N\x89&T\xfb\xb1Ie\xb8\xa7\x9a\x12\x88\xcb\xa9\x14r\x9a\xd7\xc5/\xf01\x02\x81\x81\x00\xc2B{\x893\x94/\xa6\xf8{yP\xb8t\'%\xbb"B\x97~?\xf6\xec \xf3\xfb\x91\xa6\x879\xb7\xb0|z_8\xebHv\xd3xj\xccU\x15\xff\xcf\x81nnZ`e\x96\xa0\x07e\x84u\xe01\x18!\xd2\xfe\x85\xd2\xebv\xf99\x9d\x847a\xf8N\x9e\x9f\xa1hu\x0f\t\xd1"^\xb4Y\xb8e\xe3\x98\xc08WHK\xdcs\xc5\xe5\x93<\x1f\xcd\x1f.s|>\t\xf6\xac\x80\xbb\t\x948\xeb.\x0f\x9e\x85u\x9ds\x02\x81\x80A\xc0%\x02\x17\xca\xe4\x0cE\x9a\xff\x18\xa6*\x8f\x1a\xa0\xd2f\x03*B\xf7\xccDk\xb8\xf5\xc7r\x81\x82v(\x1d\xca\xdb\xba\xca$\xf5\xa8\xd3{\xb1yQ\x91\x1bfr-\x9a{.\x1b\x8f\xcd\x9b\xf4AWS\x98\xb8\xd8\x01o\x9e\xf7c8\xc7\x97\xaa\xbd\xdc\x85\xfd\x12L\xc21w;5.\xc9\xaf6\x8d:\x8aN\x8f\xa3\x85\x02\xdc\x13Gy\xbc\xf6\x81\xcc\x0e\xef\x16\xf67\xe2*\x06\x88\x1d\xd5\xe4\'\x8f\x80\xba\xe8+\xb2\xd18\x81\x02\x81\x80R\xb4w_\xfc\x83\xb4\x9e\x03\xe0\x9d\xcf\xce\x185\xaa\x8c\xb7\x93^h3\xd7n\xc4\xc0\xdbt1P\x154\xad\x80\xf1\xa0\xa4\xdd\x17&\xef\xf5\xae\x92|\x0f7\xb0"\xcc\xdfR\xbf\x03\xc1S4\x92\xf6\x081\x80\xf5cA/w\xceJ\xcd\x86b\x0f<\x01PF\xa5BGx2\xbe\xd3\xbe<9\xc3\xd4H\xf6\x86\xfa\x95H\x114\xa7\xe5\x14`}\xfa\xb5\xea\xbd\'Y\x85/I\xd0\'\xf1\xcb\x93\xab\r\xf2\xfb \xb5\xa5\x94\xba\x01O\x1d\x02\x81\x81\x00\xbeP\n-\x1dMb\x8d\xdeG\xf7qv\x82\x02v"Y\xee\x10\x83d\xbeJ\x02\xc4\xf8]T\xe1|,\xda\xba\x80\xb9+\xc8\x8a\xe4\xf9\x8b\x9f\x8c\xaaOY\x08ghE\xe5[\x02\x8b\xc9\x16\x84j{\xb1c\xf7%\x8c\xf9\xbdZ\xc0h\xcb\xd1\xb3\x93\xb6z\xb1\xd2)c\xa1\xbe\xae\x17(i\x97\\`[v\xb7 "\x7fe\x82\xef\x92\x06\xf8 \x11-x\xec\x16\xa6\x9d\xfb}~{\xbf8V\xf0I\x94We\x17\xee\xf7\xb5\xb8\xd4H')
a_privNum = a.key.private_numbers()
-a_pubNum = a.pubkey.public_numbers()
+a_pubNum = a.pubkey.pubkey.public_numbers()
assert x_pubNum.n == x_pubNum.n
assert x_pubNum.e == x_pubNum.e
@@ -400,7 +401,7 @@ with ContextManagerCaptureOutput() as cmco:
y.show()
assert cmco.get_output().strip() == awaited.strip()
-= Cert: Check split_pem on chained certs with missing end \n
+= Cert class : Check split_pem on chained certs with missing end \n
from scapy.layers.tls.cert import split_pem
ks = split_pem(b"""
-----BEGIN EC PRIVATE KEY-----
@@ -415,7 +416,7 @@ weDU+RsFxcyU/QxD9WYORzYarqxbcA==
-----END EC PRIVATE KEY-----""")
assert ks[0][:-1] == ks[1]
-= Import PEM-encoded certificate with ed25519 signature
+= Cert class : Import PEM-encoded certificate with ed25519 signature
x = Cert("""
-----BEGIN CERTIFICATE-----
MIICqDCCAZCgAwIBAgIUYYDvh160/Q32Q/MuCGSfIYxTwwEwDQYJKoZIhvcNAQEL
@@ -436,6 +437,30 @@ XwZH9DJ6Ud0s8/j+
-----END CERTIFICATE-----
""")
+= Cert class : Change subject public key identifier and resign
+c = Cert("""
+-----BEGIN CERTIFICATE-----
+MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw
+CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
+ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg
+RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV
+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
+Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq
+hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf
+Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q
+RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD
+AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY
+JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv
+6pZjamVFkpUBtA==
+-----END CERTIFICATE-----
+""")
+k = PrivKeyECDSA()
+c.setSubjectPublicKeyFromPrivateKey(k)
+c.setSubjectPublicKeyFromPrivateKey(k.pubkey)
+c = Cert(k.resignCert(c))
+
+assert k.verifyCert(c)
########### CRL class ###############################################
From 9069bc217bc23ddfbf504351b87de3effa858e76 Mon Sep 17 00:00:00 2001
From: gpotter2 <10530980+gpotter2@users.noreply.github.com>
Date: Mon, 7 Jul 2025 13:45:15 +0200
Subject: [PATCH 6/9] CI fixes
---
doc/scapy/advanced_usage/pipetools.rst | 6 +-
scapy/fwdmachine.py | 101 ++++++++++++++-----------
scapy/layers/tls/cert.py | 9 ++-
scapy/supersocket.py | 5 +-
test/scapy/layers/tls/cert.uts | 4 +-
5 files changed, 74 insertions(+), 51 deletions(-)
diff --git a/doc/scapy/advanced_usage/pipetools.rst b/doc/scapy/advanced_usage/pipetools.rst
index 47c92d8616f..dbc7b6ce23a 100644
--- a/doc/scapy/advanced_usage/pipetools.rst
+++ b/doc/scapy/advanced_usage/pipetools.rst
@@ -34,11 +34,11 @@ The following code will sniff packets on the default interface, anonymize the so
The engine is pretty straightforward:
-.. image:: graphics/pipetool_demo.svg
+.. image:: ../graphics/pipetool_demo.svg
Let's run it:
-.. image:: graphics/animations/pipetool_demo.gif
+.. image:: ../graphics/animations/pipetool_demo.gif
Class Types
-----------
@@ -56,7 +56,7 @@ The data then goes from Drains to Drains until it arrives in a Sink, the final s
Let's see with a basic demo how to build a pipetool system.
-.. image:: graphics/pipetool_engine.png
+.. image:: ../graphics/pipetool_engine.png
For instance, this engine was generated with this code:
diff --git a/scapy/fwdmachine.py b/scapy/fwdmachine.py
index cc74db06acd..78b3976cdbf 100644
--- a/scapy/fwdmachine.py
+++ b/scapy/fwdmachine.py
@@ -7,19 +7,16 @@
Forwarding machine.
"""
-from datetime import datetime, timezone, timedelta
-
import enum
import functools
import os
import select
import socket
import ssl
-import sys
import threading
import traceback
-from scapy.asn1.asn1 import ASN1_BOOLEAN, ASN1_OID, ASN1_UTC_TIME
+from scapy.asn1.asn1 import ASN1_OID
from scapy.config import conf
from scapy.data import MTU
from scapy.packet import Packet
@@ -30,13 +27,10 @@
from scapy.layers.tls.all import (
Cert,
- Chain,
- PrivKeyRSA,
PrivKeyECDSA,
)
from scapy.layers.x509 import (
X509_AlgorithmIdentifier,
- X509_SubjectPublicKeyInfo,
)
from cryptography.hazmat.primitives import serialization
@@ -62,12 +56,13 @@ class ForwardMachine:
that they are routed through the local server, and some tweaking of the OS
routes;
- The TPROXY mode is expected to be used on a router with FORWARDING and only a specific
- set of *nat rules set to -j TPROXY. A scripts called 'vethrelay.sh' is provided in the documentation
- for setting this up.
-
- ForwardMachine supports transparently proxifying TLS. By default, it will generate lookalike self-signed
- certificates, but it's also possible to specify a certificate by using crtfile and keyfile.
+ The TPROXY mode is expected to be used on a router with FORWARDING and only a
+ specific set of *nat rules set to -j TPROXY. A script called 'vethrelay.sh'
+ is provided in the documentation for setting this up.
+
+ ForwardMachine supports transparently proxifying TLS. By default, it will generate
+ lookalike self-signed certificates, but it's also possible to specify a certificate
+ by using crtfile and keyfile.
Parameters:
@@ -75,14 +70,16 @@ class ForwardMachine:
:param cls: the scapy class to parse on that port
:param af: the address family to use (default AF_INET)
:param proto: the proto to use (default SOCK_STREAM)
- :param remote_address: the IP to use in SERVER mode, or by default in TPROXY when the destination
- is the local IP.
+ :param remote_address: the IP to use in SERVER mode, or by default in TPROXY when
+ the destination is the local IP.
:param remote_af: (optional) if provided, use a different address family to connect
to the remote host.
- :param bind_address: the IP to bind locally. "0.0.0.0" by default in SERVER mode, but "2.2.2.2" by default
- in TPROXY (if you are using the provided 'vethrelay.sh' script).
+ :param bind_address: the IP to bind locally. "0.0.0.0" by default in SERVER mode,
+ but "2.2.2.2" by default in TPROXY (if you are using the provided
+ 'vethrelay.sh' script).
:param tls: enable TLS (in both the server and client)
- :param crtfile: (optional) if provided, uses a certificate instead of self signed ones.
+ :param crtfile: (optional) if provided, uses a certificate instead of self signed
+ ones.
:param keyfile: (optional) path to the key file
:param timeout: the timeout before connecting to the real server (default 2)
@@ -164,7 +161,9 @@ def run(self):
conn, addr = self.ssock.accept()
# Calc dest
dest = conn.getsockname()
- if self.mode == ForwardMachine.MODE.SERVER or (dest[0] in self.local_ips and self.remote_address):
+ if self.mode == ForwardMachine.MODE.SERVER or (
+ dest[0] in self.local_ips and self.remote_address
+ ):
dest = (self.remote_address,) + dest[1:]
print(self.ct.green("%s -> %s connected !" % (repr(addr), repr(dest))))
try:
@@ -172,7 +171,7 @@ def run(self):
target=self.handler,
args=(conn, addr, dest),
).start()
- except Exception as ex:
+ except Exception:
print(self.ct.red("%s errored !" % repr(addr)))
conn.close()
pass
@@ -283,17 +282,20 @@ def gen_alike_chain(self, certs, privkey):
# Set SubjectPublicKeyInfo to the one from our private key
c.setSubjectPublicKeyFromPrivateKey(privkey)
# Filter out extensions that would cause trouble
- c.tbsCertificate.serialNumber.val = int(RandInt()) # otherwise SEC_ERROR_REUSED_ISSUER_AND_SERIAL
+ c.tbsCertificate.serialNumber.val = int(
+ RandInt()
+ ) # otherwise SEC_ERROR_REUSED_ISSUER_AND_SERIAL
c.tbsCertificate.extensions = [
x
for x in c.tbsCertificate.extensions
- if x.extnID not in [
- '2.5.29.32', # CPS
- '2.5.29.31', # cRLDistributionPoints
- '1.3.6.1.5.5.7.1.1', # authorityInfoAccess
- '1.3.6.1.4.1.11129.2.4.2', # SCT
- '2.5.29.14', # subjectKeyIdentifier
- '2.5.29.35', # authorityKeyIdentifier
+ if x.extnID
+ not in [
+ "2.5.29.32", # CPS
+ "2.5.29.31", # cRLDistributionPoints
+ "1.3.6.1.5.5.7.1.1", # authorityInfoAccess
+ "1.3.6.1.4.1.11129.2.4.2", # SCT
+ "2.5.29.14", # subjectKeyIdentifier
+ "2.5.29.35", # authorityKeyIdentifier
]
]
# For now, we only provide a RSA private key, so we can only sign with that :/
@@ -315,20 +317,19 @@ def get_key_and_alike_chain(self, cas, dest, server_name):
ident = server_name or dest
if ident in self.cache:
return self.cache[ident]
- # Parse CAs
+ # Parse CAs
certs = [Cert(c.public_bytes()) for c in cas]
# certs = certs[:1]
# Generate Private Key
privkey = PrivKeyECDSA()
# Iterate
certs = self.gen_alike_chain(certs, privkey)
- # Build a chain object. This checks that everything is properly signed, and re-order
- # the certs.
+ # Build a chain object. This checks that everything is properly signed, and
+ # re-order the certs.
# chain = Chain(certs, cert0=certs[-1])
self.cache[ident] = privkey, certs
return privkey, certs
-
def handler(self, sock, addr, dest):
"""
Handler of a client socket
@@ -346,7 +347,8 @@ def handler(self, sock, addr, dest):
# This acts as follows:
# - start the server-side TLS handshake
- # - use the SNI callback to pop a client-side socket (using the real provided SNI)
+ # - use the SNI callback to pop a client-side socket (using the real
+ # provided SNI)
# - serve the certificate
_clisock = [ss]
@@ -361,12 +363,12 @@ def cb_sni(sock, server_name, _):
ss = clisslcontext.wrap_socket(ss, server_hostname=server_name)
# Get certificate chain
cas = ss._sslobj.get_unverified_chain()
- # Get negotiated cipher type
- cipher = ss.shared_ciphers()
if self.crtfile is None:
# SELF-SIGNED mode
# Generate private key based on the type of certificate
- privkey, certs = self.get_key_and_alike_chain(cas, dest, server_name)
+ privkey, certs = self.get_key_and_alike_chain(
+ cas, dest, server_name
+ )
# Load result certificate our SSL server
# (this is dumb but we need to store them on disk)
certfile = get_temp_file()
@@ -376,11 +378,15 @@ def cb_sni(sock, server_name, _):
keyfile = get_temp_file()
with open(keyfile, "wb") as fd:
password = os.urandom(32)
- fd.write(privkey.key.private_bytes(
- encoding=serialization.Encoding.PEM,
- format=serialization.PrivateFormat.PKCS8,
- encryption_algorithm=serialization.BestAvailableEncryption(password),
- ))
+ fd.write(
+ privkey.key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.BestAvailableEncryption( # noqa: E501
+ password
+ ),
+ )
+ )
else:
# Certificate is provided
certfile = self.crtfile
@@ -432,7 +438,11 @@ def cb_sni(sock, server_name, _):
try:
func(data, ctx)
# If this doesn't raise, it's a user error.
- print(self.ct.red("%s ERROR: you must always raise in %s !" % func))
+ print(
+ self.ct.red(
+ "%s ERROR: you must always raise in %s !" % func
+ )
+ )
return
except self.REDIRECT_TO as ex:
# Replace the peer socket with a new socket
@@ -441,7 +451,10 @@ def cb_sni(sock, server_name, _):
ex.dest, server_hostname=ex.server_hostname
)
ss = self.sockcls(ss, self.cls)
- print("C: %s redirected to %s" % (repr(ctx.dest), repr(ex.dest)))
+ print(
+ "C: %s redirected to %s"
+ % (repr(ctx.dest), repr(ex.dest))
+ )
ctx.dest = ex.dest # update context
# Shut the old one.
oldss.ins.shutdown(socket.SHUT_RDWR)
@@ -472,7 +485,7 @@ def cb_sni(sock, server_name, _):
# Processing failed. forward to not break anything
print(
self.ct.orange(
- "Exception happend in handling client %s ! (forwarding.)"
+ "Exception happened in handling client %s ! (forward)"
% repr(addr)
)
)
diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py
index dd278df545f..566d73dce46 100644
--- a/scapy/layers/tls/cert.py
+++ b/scapy/layers/tls/cert.py
@@ -164,7 +164,7 @@ def __call__(cls, obj_path, obj_max_size, pem_marker=None):
if b"-----BEGIN" in _raw:
frmt = "PEM"
pem = _raw
- der_list = split_pem(_raw)
+ der_list = split_pem(pem)
der = b''.join(map(pem2der, der_list))
else:
frmt = "DER"
@@ -277,6 +277,13 @@ def der(self):
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
+ def public_numbers(self, *args, **kwargs):
+ return self.pubkey.public_numbers(*args, **kwargs)
+
+ @property
+ def key_size(self):
+ return self.pubkey.key_size
+
def export(self, filename, fmt="DER"):
"""
Export public key in 'fmt' format (DER or PEM) to file 'filename'
diff --git a/scapy/supersocket.py b/scapy/supersocket.py
index a6009261616..5ffd7a30f63 100644
--- a/scapy/supersocket.py
+++ b/scapy/supersocket.py
@@ -542,7 +542,10 @@ def select(sockets, remain=None):
]
if queued:
return queued # type: ignore
- return super(StreamSocketPeekless, StreamSocketPeekless).select(sockets, remain=remain)
+ return super(StreamSocketPeekless, StreamSocketPeekless).select(
+ sockets,
+ remain=remain,
+ )
# Old name: SSLStreamSocket
diff --git a/test/scapy/layers/tls/cert.uts b/test/scapy/layers/tls/cert.uts
index 4ef835a140e..f0a258e4db4 100644
--- a/test/scapy/layers/tls/cert.uts
+++ b/test/scapy/layers/tls/cert.uts
@@ -108,7 +108,7 @@ W3a3ICJ/ZYLvkgb4IBEteOwWpp37fX57vzhW8EmUV2UX7ve1uNRI
-----END RSA PRIVATE KEY-----
""")
x_privNum = x.key.private_numbers()
-x_pubNum = x.pubkey.pubkey.public_numbers()
+x_pubNum = x.pubkey.public_numbers()
type(x) is PrivKeyRSA
= PrivKey class : Checking public attributes
@@ -159,7 +159,7 @@ assert not b.verify(b"Hello", data)
= PrivKey class : Importing DER-encoded RSA private key
a = PrivKeyRSA(b'0\x82\x04\xa3\x02\x01\x00\x02\x82\x01\x01\x00\x98Wj?\xe9\xd3\x11\x9b\xa4KIK?\xec\xa3\xd6\x03H\x9a\xc1\x08\x7f\xb3\xf6\xc9$\xee\x9d\xc7\x98\xc7\t&\xe1Q9A\x17\x83m\xbd\x8b\xe7\xf1\xcb\x03\xdaO\x98\x87\x90-*\xbf\x06\x03\xb5\x99\xe3\x9cl\xe4\x89\xd9\x85GCo\x0cC\x9e\xbe\xf0*\xdb\xea}\xbc\x8b\'\x17\xe2\x1at\x1fp1D\x08\xe1\xd1\xe7W\xfa\xad\xf2\x8a[\xd8\'\x85\xbd\xfc\xd9\xc8o\xfc\x00g\x04\xb4\xa0\x98\x9f\xfe\xd4\xe4T^\xfb\x1f&\xc0|\x97^\xe4J\x9b\xa7\xe6\xc2(\x8b\xccZv\xa6n\x1fCEL\xa3\xac\x10Y\xa3\x97@\xd6\x8d\xf6\xce\x9b\x85\x06\xb2]#\xc7fR\x9c=\x82\xd7\xf4\x17@Z\xf2Q\x99\x9b\xc5*sA\xb2]\xe5\xce%A6\xbb\xb0\xa22\xed\xcc\xef\xb0L\xe9\x92\xcbM\xca0\xe7\xe6\xd0"i&L\xbdR\x1a\x1c\xf0~)\xcc\x13W\xba\xa7q\xe6\xff\xfaC\x8e\xe2o\x15\xa66\xdaM9.\x02\xee\xca\xa79\xf6\xf1b\x07t\xe8\x95\xdc\xfc\xf8\x06\xcc6;\xf3\x03\x02\x03\x01\x00\x01\x02\x82\x01\x00}\xcax\x96K\xda\x18H\xffQ\x974\xc6\x94\xfc\xf7\xc3\x80Y \x99\x86\xf10\x0f\t*\xeb\'\x9b\xf4\x85\x8f\x100\x04i\xc6#\xa5#\x05zA\x82\x94,\xd8\xda\xa6\xdd\x9b\x1e\x17\xdb\xbc\x86`\x8a\xbch\x82\x11}\x86z\xc0\xa8\xdad\x9f\x99$1\x0f\xa4A\xac\xc4\xeeC\xdfT^\x9cs\x04\x8b\x1c\x16s?f\xbb<\x94\xf0@Dl\xe6\x17i\xc8\xde\xa3\xf1^\xd7\xb1\xe0\x00W\xe6\x8d\x02w\x83_fVc\xa6?z\xb2E(;\xcf\x9bwr88\xf5\x05`QE\xba\xff5.\x84\x90\xe8w\xb0\xd1\xaf1\xb4\x95\xed*@\xe9\xf6\x1dcLa\x949\xcd|\xf5`\x026\x80K\x1fC\x06yZ\x12Q\xf75\xfb\xe4\\\x0bw2\xa5F||\xd3\xf58\xee\x86\x05\xfb\xf4\xe9\xfeh\\D\xb8e\xbcFb\x96\xbe\x9c\'\xcd\xc1\xbe\x95\xda\x05\x9b\x92\xf6\x98\x9b\xb6\x85\x9fB\x96\x18\xd6WKS\xf79\xc5\xaf\xe8\x85\x9d:h6\x08\x8e\xcc0\x12\xc9\xd0\xbc\x96T!\x02\x81\x81\x00\xc8\xc2X\xc3_y\x9a\xf1\xe5\xf1\\\xb1\'q\xe4\xc5\xe4\x1aj\x8c\xd6/\x8a\xa15\r\nk\x0f\xa6\xcc?\xa3a\xd9\x1b\x1d\xbd\xda\x05\x88!\xb2\x01\x03f9\xf29V\xe5\x0b\xb2>\xe2\xc9\xaf\x01\x92KX\x96\x9f\xea\xc6y\x1c\x0c7\xceh\xf5\x89\xb0\xa4_\x1e\xddz\x84\\\xfd\x05\\:)%\xdd\xd1?\xcac\xb4(bM\x88l\xc9fl[E-\xe1N\x89&T\xfb\xb1Ie\xb8\xa7\x9a\x12\x88\xcb\xa9\x14r\x9a\xd7\xc5/\xf01\x02\x81\x81\x00\xc2B{\x893\x94/\xa6\xf8{yP\xb8t\'%\xbb"B\x97~?\xf6\xec \xf3\xfb\x91\xa6\x879\xb7\xb0|z_8\xebHv\xd3xj\xccU\x15\xff\xcf\x81nnZ`e\x96\xa0\x07e\x84u\xe01\x18!\xd2\xfe\x85\xd2\xebv\xf99\x9d\x847a\xf8N\x9e\x9f\xa1hu\x0f\t\xd1"^\xb4Y\xb8e\xe3\x98\xc08WHK\xdcs\xc5\xe5\x93<\x1f\xcd\x1f.s|>\t\xf6\xac\x80\xbb\t\x948\xeb.\x0f\x9e\x85u\x9ds\x02\x81\x80A\xc0%\x02\x17\xca\xe4\x0cE\x9a\xff\x18\xa6*\x8f\x1a\xa0\xd2f\x03*B\xf7\xccDk\xb8\xf5\xc7r\x81\x82v(\x1d\xca\xdb\xba\xca$\xf5\xa8\xd3{\xb1yQ\x91\x1bfr-\x9a{.\x1b\x8f\xcd\x9b\xf4AWS\x98\xb8\xd8\x01o\x9e\xf7c8\xc7\x97\xaa\xbd\xdc\x85\xfd\x12L\xc21w;5.\xc9\xaf6\x8d:\x8aN\x8f\xa3\x85\x02\xdc\x13Gy\xbc\xf6\x81\xcc\x0e\xef\x16\xf67\xe2*\x06\x88\x1d\xd5\xe4\'\x8f\x80\xba\xe8+\xb2\xd18\x81\x02\x81\x80R\xb4w_\xfc\x83\xb4\x9e\x03\xe0\x9d\xcf\xce\x185\xaa\x8c\xb7\x93^h3\xd7n\xc4\xc0\xdbt1P\x154\xad\x80\xf1\xa0\xa4\xdd\x17&\xef\xf5\xae\x92|\x0f7\xb0"\xcc\xdfR\xbf\x03\xc1S4\x92\xf6\x081\x80\xf5cA/w\xceJ\xcd\x86b\x0f<\x01PF\xa5BGx2\xbe\xd3\xbe<9\xc3\xd4H\xf6\x86\xfa\x95H\x114\xa7\xe5\x14`}\xfa\xb5\xea\xbd\'Y\x85/I\xd0\'\xf1\xcb\x93\xab\r\xf2\xfb \xb5\xa5\x94\xba\x01O\x1d\x02\x81\x81\x00\xbeP\n-\x1dMb\x8d\xdeG\xf7qv\x82\x02v"Y\xee\x10\x83d\xbeJ\x02\xc4\xf8]T\xe1|,\xda\xba\x80\xb9+\xc8\x8a\xe4\xf9\x8b\x9f\x8c\xaaOY\x08ghE\xe5[\x02\x8b\xc9\x16\x84j{\xb1c\xf7%\x8c\xf9\xbdZ\xc0h\xcb\xd1\xb3\x93\xb6z\xb1\xd2)c\xa1\xbe\xae\x17(i\x97\\`[v\xb7 "\x7fe\x82\xef\x92\x06\xf8 \x11-x\xec\x16\xa6\x9d\xfb}~{\xbf8V\xf0I\x94We\x17\xee\xf7\xb5\xb8\xd4H')
a_privNum = a.key.private_numbers()
-a_pubNum = a.pubkey.pubkey.public_numbers()
+a_pubNum = a.pubkey.public_numbers()
assert x_pubNum.n == x_pubNum.n
assert x_pubNum.e == x_pubNum.e
From 4c3b4406fdd8b97631d38da9a125e42c8e2f9b07 Mon Sep 17 00:00:00 2001
From: gpotter2 <10530980+gpotter2@users.noreply.github.com>
Date: Mon, 7 Jul 2025 13:55:15 +0200
Subject: [PATCH 7/9] Fix TLS 1.3 exchange
---
scapy/layers/tls/keyexchange_tls13.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/scapy/layers/tls/keyexchange_tls13.py b/scapy/layers/tls/keyexchange_tls13.py
index d9ddda437a6..03692ffdccb 100644
--- a/scapy/layers/tls/keyexchange_tls13.py
+++ b/scapy/layers/tls/keyexchange_tls13.py
@@ -333,9 +333,9 @@ def get_usable_tls13_sigalgs(li, key, location="certificateverify"):
elif isinstance(key, PrivKeyECDSA):
kx = "ecdsa"
elif isinstance(key, PrivKeyEdDSA):
- if isinstance(key.pubkey, ed25519.Ed25519PublicKey):
+ if isinstance(key.pubkey.pubkey, ed25519.Ed25519PublicKey):
kx = "ed25519"
- elif isinstance(key.pubkey, ed448.Ed448PublicKey):
+ elif isinstance(key.pubkey.pubkey, ed448.Ed448PublicKey):
kx = "ed448"
else:
kx = "unknown"
From e23b1d5b2294025b1cbf0019f133bdabe7b20e25 Mon Sep 17 00:00:00 2001
From: gpotter2 <10530980+gpotter2@users.noreply.github.com>
Date: Mon, 7 Jul 2025 13:56:59 +0200
Subject: [PATCH 8/9] Fix doc
---
scapy/fwdmachine.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/scapy/fwdmachine.py b/scapy/fwdmachine.py
index 78b3976cdbf..04060b02cc3 100644
--- a/scapy/fwdmachine.py
+++ b/scapy/fwdmachine.py
@@ -51,13 +51,13 @@ class ForwardMachine:
two modes:
- SERVER: the server binds a port on its local IP and forwards packets to a
- `remote_address`.
+ ``remote_address``.
- TPROXY: the server binds can intercept packets to any IP destination, provided
that they are routed through the local server, and some tweaking of the OS
routes;
The TPROXY mode is expected to be used on a router with FORWARDING and only a
- specific set of *nat rules set to -j TPROXY. A script called 'vethrelay.sh'
+ specific set of nat rules set to -j TPROXY. A script called 'vethrelay.sh'
is provided in the documentation for setting this up.
ForwardMachine supports transparently proxifying TLS. By default, it will generate
From 729993af9253303cbe42ce92be1df368e5efd268 Mon Sep 17 00:00:00 2001
From: gpotter2 <10530980+gpotter2@users.noreply.github.com>
Date: Mon, 7 Jul 2025 14:02:46 +0200
Subject: [PATCH 9/9] Make pem format string instead of bytes
---
scapy/layers/tls/cert.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/scapy/layers/tls/cert.py b/scapy/layers/tls/cert.py
index 566d73dce46..6b0dc24bfa4 100644
--- a/scapy/layers/tls/cert.py
+++ b/scapy/layers/tls/cert.py
@@ -84,11 +84,11 @@
def der2pem(der_string, obj="UNKNOWN"):
"""Convert DER octet string to PEM format (with optional header)"""
# Encode a byte string in PEM format. Header advertises type.
- pem_string = ("-----BEGIN %s-----\n" % obj).encode()
- base64_string = base64.b64encode(der_string)
+ pem_string = "-----BEGIN %s-----\n" % obj
+ base64_string = base64.b64encode(der_string).decode()
chunks = [base64_string[i:i + 64] for i in range(0, len(base64_string), 64)] # noqa: E501
- pem_string += b'\n'.join(chunks)
- pem_string += ("\n-----END %s-----\n" % obj).encode()
+ pem_string += '\n'.join(chunks)
+ pem_string += "\n-----END %s-----\n" % obj
return pem_string