Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 28 additions & 21 deletions doc/scapy/_static/vethrelay.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/bin/bash

# Setup iptables for IP relay by creating an interface configured
# to be the destination of TPROXY rules.
# Setup nftables for IP relay by creating an interface configured
# to be the destination of TPROXY rules. All nft rules live in a
# dedicated 'scapy-tproxy' table.

if [ "$EUID" -ne 0 ]
then echo "Please run as root"
Expand All @@ -15,18 +16,19 @@ fi

IFACE="vethrelay"
IP="2.2.2.2"
NFT_TABLE="scapy-tproxy"

# 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
nft list table ip "$NFT_TABLE" >/dev/null 2>&1
return $?
}

if [ "$1" == "setup" ]; then
# Add "DIVERT" chain if it doesn't exist
# Add the scapy-tproxy table if it doesn't exist
checkSetup
if [ $? -eq 0 ]; then
echo "vethrelay already setup !"
Expand All @@ -37,13 +39,18 @@ if [ "$1" == "setup" ]; then
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
# All TPROXY rules live in a dedicated nftables table so that
# iptables-nft (which manages the ip filter/nat/mangle tables)
# is left untouched.
# The DIVERT chain is an optimisation. The socket match catches
# packets from already established sockets. Those are marked as 1
# then accepted directly so that TPROXY does not run again.
nft "add table ip "$NFT_TABLE""
nft "add chain ip "$NFT_TABLE" DIVERT"
nft "add chain ip "$NFT_TABLE" PREROUTING { type filter hook prerouting priority mangle; policy accept; }"
nft "add rule ip "$NFT_TABLE" PREROUTING ip protocol tcp socket wildcard 0 jump DIVERT"
nft "add rule ip "$NFT_TABLE" DIVERT meta mark set 0x1"
nft "add rule ip "$NFT_TABLE" DIVERT accept"
# Packets marked with 1 are routed through table 100 instead of the
# default routing table
ip rule add fwmark 1 lookup 100
Expand All @@ -52,23 +59,23 @@ if [ "$1" == "setup" ]; then
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 "nft add rule ip "$NFT_TABLE" PREROUTING tcp dport 80 meta mark set 0x1 tproxy ip to $IP:8080 accept"
echo
echo "# Listen on wlp4s0 for incoming packets on port 80 (on the interface where it really comes from)"
echo "# Note: you need to allow INPUT on the port that you are adding a listening rule on. For instance, to listen"
echo "# on wlp4s0 for incoming packets on port 80 (on the interface where it really comes from), one can do"
echo "nft add rule ip <mytable> INPUT iifname <wlp4s0 tcp dport 80 accept"
echo "# or using iptables"
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
# Remove all setup rules by deleting the whole nftables table
ip rule del fwmark 1 lookup 100
ip route del local 0.0.0.0/0 dev $IFACE table 100
nft delete table ip "$NFT_TABLE"
ip link del dev $IFACE
echo -e "\x1b[32mInterface $IFACE unsetup !\x1b[0m"
fi
2 changes: 2 additions & 0 deletions scapy/base_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ def __contains__(self, other):
return self.start <= other <= self.stop
if isinstance(other, str):
return self.__class__(other) in self
if other == "%":
return isinstance(self, (Net, _ScopedIP)) and bool(self.scope)
if type(other) is not self.__class__:
return False
return self.start <= other.start <= other.stop <= self.stop
Expand Down
74 changes: 49 additions & 25 deletions scapy/fwdmachine.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from scapy.layers.tls.all import (
Cert,
CertTree,
PrivKeyECDSA,
)
from scapy.layers.x509 import (
Expand Down Expand Up @@ -135,10 +136,6 @@ def __init__(
self.MTU = MTU
self.remote_address = remote_address
self.remote_port = remote_port
if self.tls or self.af == 40: # TLS or VSOCK
self.sockcls = StreamSocketPeekless
else:
self.sockcls = StreamSocket
# Chose 'bind_address' depending on the mode
self.bind_address = bind_address
if self.bind_address is None:
Expand Down Expand Up @@ -233,10 +230,21 @@ class CONTEXT:
CONTEXT object kept during a session
"""

def __init__(self, fwdm, addr, dest):
def __init__(self, fwdm, addr, dest, remote_af, proto, tls, cls):
self.addr = addr
self.dest = dest
self.remote_af = remote_af
self.proto = proto
self.tls = tls
self.tls_sni_name = None # Retrieved when receiving a connection
self.cls = cls

@property
def sockcls(self):
if self.tls or self.remote_af == 40: # TLS or VSOCK
return StreamSocketPeekless
else:
return StreamSocket

def vprint(self, evt, ctx, cs, req, rep):
if evt == self.FORWARD:
Expand Down Expand Up @@ -290,7 +298,7 @@ def _getpeersock(self, dest, ctx, server_hostname=None):
"""
Get peer socket
"""
s = socket.socket(self.remote_af, self.proto)
s = socket.socket(ctx.remote_af, ctx.proto)
s.settimeout(self.timeout)
ndest = self.destalias(dest)
if ndest != dest:
Expand All @@ -304,15 +312,21 @@ 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:]

# Set SubjectPublicKeyInfo to the one from our private key
c.setSubjectPublicKeyFromPrivateKey(privkey)
c.updateSubjectKeyIdentifier()

if certs:
# Recursive: if there are certificates above this one in the chain, do them
# first.
certs = self.gen_alike_chain(certs, privkey)
c.updateAuthorityKeyIdentifier(certs[0])
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)
c.updateAuthorityKeyIdentifier(c)

# Filter out extensions that would cause trouble
c.tbsCertificate.serialNumber.val = int(
RandInt()
Expand All @@ -326,8 +340,6 @@ def gen_alike_chain(self, certs, privkey):
"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 :/
Expand All @@ -351,27 +363,38 @@ def get_key_and_alike_chain(self, cas, dest, server_name):
return self.cache[ident]
# Parse CAs
certs = [Cert(c.public_bytes()) for c in cas]
# certs = certs[:1]
# XXX - Only get last cert. We shouldn't require this
# but we need to add support for multiple private keys...
chain = CertTree(certs, certs)
certs = [chain.getleaves()[0]]
# 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(self, addr, dest) # we have a context object
# we have a context object
ctx = self.CONTEXT(
self,
addr=addr,
dest=dest,
remote_af=self.remote_af,
proto=self.proto,
tls=self.tls,
cls=self.cls,
)
self.newconn(ctx)

# Initialize peer socket
ss = self._getpeersock(dest, ctx)

# Wrap both server and peer sockets in SSL
if self.tls:
if ctx.tls:
# Build client SSL context
clisslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS)
clisslcontext.load_default_certs()
Expand Down Expand Up @@ -425,7 +448,7 @@ def cb_sni(sock, server_name, _):
password = self.keyfilepwd
certfile = self.crtfile
keyfile = self.keyfile
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS)
sslcontext.check_hostname = False
sslcontext.verify_mode = ssl.CERT_NONE # note: server side
sslcontext.load_cert_chain(certfile, keyfile, password=password)
Expand All @@ -435,7 +458,7 @@ def cb_sni(sock, server_name, _):
return None # Continue

# Server SSL context
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS)
sslcontext.sni_callback = cb_sni
try:
sock = sslcontext.wrap_socket(sock, server_side=True)
Expand All @@ -445,8 +468,8 @@ def cb_sni(sock, server_name, _):
return
ss = _clisock[0]
# Wrap the sockets
sock = self.sockcls(sock, self.cls)
ss = self.sockcls(ss, self.cls)
sock = ctx.sockcls(sock, ctx.cls)
ss = ctx.sockcls(ss, ctx.cls)
sock.streamsession = ss.streamsession
try:
while True:
Expand All @@ -463,8 +486,9 @@ def cb_sni(sock, server_name, _):
# get data
try:
data = thissock.recv(self.MTU)
except EOFError:
raise RuntimeError
except EOFError as ex:
othersock.ins.shutdown(socket.SHUT_RDWR)
raise RuntimeError(str(ex))
if not data:
# Session needs more data
continue
Expand All @@ -489,7 +513,7 @@ def cb_sni(sock, server_name, _):
ctx,
server_hostname=ex.server_hostname,
)
ss = self.sockcls(ss, self.cls)
ss = ctx.sockcls(ss, ctx.cls)
self.vprint(self.REDIRECT_TO, ctx, 1, ctx.dest, ex.dest)
ctx.dest = ex.dest # update context
# Shut the old one.
Expand Down Expand Up @@ -529,8 +553,8 @@ def cb_sni(sock, server_name, _):
traceback.print_exception(ex)
othersock.send(data)
self.vprint(self.FORWARD, ctx, cs, data, None)
except RuntimeError:
print(self.ct.red("%s DISCONNECTED !" % repr(addr)))
except RuntimeError as ex:
print(self.ct.red("%s DISCONNECTED !: %s" % (repr(addr), str(ex))))
self.delconn(ctx)
sock.close()
ss.close()
15 changes: 9 additions & 6 deletions scapy/layers/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,10 @@ def do_dissect(self, s):
return body

def mysummary(self):
return self.sprintf("%HTTPRequest.Method% '%HTTPRequest.Path%' ")
if self.Host:
return self.sprintf("%HTTPRequest.Method% '%HTTPRequest.Path%' [%Host%]")
else:
return self.sprintf("%HTTPRequest.Method% '%HTTPRequest.Path%'")


class HTTPResponse(_HTTPContent):
Expand Down Expand Up @@ -687,8 +690,8 @@ def tcp_reassemble(cls, data, metadata, session):
# use it. When the total size of the frags is high enough,
# we have the packet

if session.pop("head_request", False):
# Answer to a HEAD request.
if session.pop("simple_request", False):
# Answer to a HEAD/CONNECT request.
detect_end = lambda dat: dat.find(b"\r\n\r\n")

# Subtract the length of the "HTTP*" layer
Expand Down Expand Up @@ -719,12 +722,12 @@ def tcp_reassemble(cls, data, metadata, session):
metadata["detect_unknown"] = True
if (
isinstance(http_packet.payload, cls.clsreq)
and http_packet.Method == b"HEAD"
and http_packet.Method in [b"HEAD", b"CONNECT"]
):
session["head_request"] = True
session["simple_request"] = True
elif is_response and (
http_packet.Status_Code == b"101"
or session.pop("head_request", False)
or session.pop("simple_request", False)
):
# If it's an upgrade response, it may also hold a
# different protocol data. make sure all headers are present
Expand Down
Loading
Loading