2025-12-25 upload

This commit is contained in:
“shengyudong”
2025-12-25 11:16:59 +08:00
commit 322ac74336
2241 changed files with 639966 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import sys as _sys
from .consts import Layer, Flag, Param, CalcChecksumsOption, Direction, Protocol
from .packet import Packet
from .windivert import WinDivert
__author__ = 'fabio'
__version__ = '2.1.0'
if _sys.version_info < (3, 4):
# add socket.inet_pton on Python < 3.4
import win_inet_pton as _win_inet_pton
assert _win_inet_pton
__all__ = [
"WinDivert",
"Packet",
"Layer", "Flag", "Param", "CalcChecksumsOption", "Direction", "Protocol",
]

View File

@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from enum import IntEnum
# Divert layers.
class Layer(IntEnum):
"""
See https://reqrypt.org/windivert-doc.html#divert_open
"""
NETWORK = 0
NETWORK_FORWARD = 1
# Divert Flag.
class Flag(IntEnum):
"""
See https://reqrypt.org/windivert-doc.html#divert_open
"""
DEFAULT = 0
SNIFF = 1
DROP = 2
NO_CHECKSUM = 1024 # Deprecated since Windivert 1.2
# Divert parameters.
class Param(IntEnum):
"""
See https://reqrypt.org/windivert-doc.html#divert_set_param
"""
QUEUE_LEN = 0 # Packet queue length 1 < default 512 (actually 1024) < 8192
QUEUE_TIME = 1 # Packet queue time 128 < default 512 < 2048
QUEUE_SIZE = 2 # Packet queue size (bytes) 4096 (4KB) < default 4194304 (4MB) < 33554432 (32MB)
# Direction outbound/inbound
class Direction(IntEnum):
"""
See https://reqrypt.org/windivert-doc.html#divert_address
"""
OUTBOUND = 0
INBOUND = 1
# Checksums
class CalcChecksumsOption(IntEnum):
"""
See https://reqrypt.org/windivert-doc.html#divert_helper_calc_checksums
"""
NO_IP_CHECKSUM = 1
NO_ICMP_CHECKSUM = 2
NO_ICMPV6_CHECKSUM = 4
NO_TCP_CHECKSUM = 8
NO_UDP_CHECKSUM = 16
NO_REPLACE = 2048
class Protocol(IntEnum):
"""
Transport protocol values define the layout of the header that will immediately follow the IPv4 or IPv6 header.
See http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
"""
HOPOPT = 0
ICMP = 1
TCP = 6
UDP = 17
ROUTING = 43
FRAGMENT = 44
AH = 51
ICMPV6 = 58
NONE = 59
DSTOPTS = 60
IPV6_EXT_HEADERS = {
Protocol.HOPOPT,
Protocol.ROUTING,
Protocol.FRAGMENT,
Protocol.DSTOPTS,
Protocol.AH,
}

View File

@@ -0,0 +1,350 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import ctypes
import pprint
import socket
from pydivert import windivert_dll
from pydivert.consts import Direction, IPV6_EXT_HEADERS, Protocol, Layer
from pydivert.packet.header import Header
from pydivert.packet.icmp import ICMPv4Header, ICMPv6Header
from pydivert.packet.ip import IPv4Header, IPv6Header
from pydivert.packet.tcp import TCPHeader
from pydivert.packet.udp import UDPHeader
from pydivert.util import cached_property, indexbyte as i, PY2
class Packet(object):
"""
A single packet, possibly including an IP header, a TCP/UDP header and a payload.
Creation of packets is cheap, parsing is done on first attribute access.
"""
def __init__(self, raw, interface, direction):
if isinstance(raw, bytes):
raw = memoryview(bytearray(raw))
self.raw = raw # type: memoryview
self.interface = interface
self.direction = direction
def __repr__(self):
def dump(x):
if isinstance(x, Header) or isinstance(x, Packet):
d = {}
for k in dir(x):
v = getattr(x, k)
if k.startswith("_") or callable(v):
continue
if k in {"address_family", "protocol", "ip", "icmp"}:
continue
if k == "payload" and v and len(v) > 20:
v = v[:20] + b"..."
d[k] = dump(v)
if isinstance(x, Packet):
return pprint.pformat(d)
return d
return x
return "Packet({})".format(dump(self))
@property
def is_outbound(self):
"""
Indicates if the packet is outbound.
Convenience method for ``.direction``.
"""
return self.direction == Direction.OUTBOUND
@property
def is_inbound(self):
"""
Indicates if the packet is inbound.
Convenience method for ``.direction``.
"""
return self.direction == Direction.INBOUND
@property
def is_loopback(self):
"""
- True, if the packet is on the loopback interface.
- False, otherwise.
"""
return self.interface[0] == 1
@cached_property
def address_family(self):
"""
The packet address family:
- socket.AF_INET, if IPv4
- socket.AF_INET6, if IPv6
- None, otherwise.
"""
if len(self.raw) >= 20:
v = i(self.raw[0]) >> 4
if v == 4:
return socket.AF_INET
if v == 6:
return socket.AF_INET6
@cached_property
def protocol(self):
"""
- | A (ipproto, proto_start) tuple.
| ``ipproto`` is the IP protocol in use, e.g. Protocol.TCP or Protocol.UDP.
| ``proto_start`` denotes the beginning of the protocol data.
| If the packet does not match our expectations, both ipproto and proto_start are None.
"""
if self.address_family == socket.AF_INET:
proto = i(self.raw[9])
start = (i(self.raw[0]) & 0b1111) * 4
elif self.address_family == socket.AF_INET6:
proto = i(self.raw[6])
# skip over well-known ipv6 headers
start = 40
while proto in IPV6_EXT_HEADERS:
if start >= len(self.raw):
# less than two bytes left
start = None
proto = None
break
if proto == Protocol.FRAGMENT:
hdrlen = 8
elif proto == Protocol.AH:
hdrlen = (i(self.raw[start + 1]) + 2) * 4
else:
# Protocol.HOPOPT, Protocol.DSTOPTS, Protocol.ROUTING
hdrlen = (i(self.raw[start + 1]) + 1) * 8
proto = i(self.raw[start])
start += hdrlen
else:
start = None
proto = None
out_of_bounds = (
(proto == Protocol.TCP and start + 20 > len(self.raw)) or
(proto == Protocol.UDP and start + 8 > len(self.raw)) or
(proto in {Protocol.ICMP, Protocol.ICMPV6} and start + 4 > len(self.raw))
)
if out_of_bounds:
# special-case tcp/udp so that we can rely on .protocol for the port properties.
start = None
proto = None
return proto, start
@cached_property
def ipv4(self):
"""
- An IPv4Header instance, if the packet is valid IPv4.
- None, otherwise.
"""
if self.address_family == socket.AF_INET:
return IPv4Header(self)
@cached_property
def ipv6(self):
"""
- An IPv6Header instance, if the packet is valid IPv6.
- None, otherwise.
"""
if self.address_family == socket.AF_INET6:
return IPv6Header(self)
@cached_property
def ip(self):
"""
- An IPHeader instance, if the packet is valid IPv4 or IPv6.
- None, otherwise.
"""
return self.ipv4 or self.ipv6
@cached_property
def icmpv4(self):
"""
- An ICMPv4Header instance, if the packet is valid ICMPv4.
- None, otherwise.
"""
ipproto, proto_start = self.protocol
if ipproto == Protocol.ICMP:
return ICMPv4Header(self, proto_start)
@cached_property
def icmpv6(self):
"""
- An ICMPv6Header instance, if the packet is valid ICMPv6.
- None, otherwise.
"""
ipproto, proto_start = self.protocol
if ipproto == Protocol.ICMPV6:
return ICMPv6Header(self, proto_start)
@cached_property
def icmp(self):
"""
- An ICMPHeader instance, if the packet is valid ICMPv4 or ICMPv6.
- None, otherwise.
"""
return self.icmpv4 or self.icmpv6
@cached_property
def tcp(self):
"""
- An TCPHeader instance, if the packet is valid TCP.
- None, otherwise.
"""
ipproto, proto_start = self.protocol
if ipproto == Protocol.TCP:
return TCPHeader(self, proto_start)
@cached_property
def udp(self):
"""
- An TCPHeader instance, if the packet is valid UDP.
- None, otherwise.
"""
ipproto, proto_start = self.protocol
if ipproto == Protocol.UDP:
return UDPHeader(self, proto_start)
@cached_property
def _port(self):
"""header that implements PortMixin"""
return self.tcp or self.udp
@cached_property
def _payload(self):
"""header that implements PayloadMixin"""
return self.tcp or self.udp or self.icmpv4 or self.icmpv6
@property
def src_addr(self):
"""
- The source address, if the packet is valid IPv4 or IPv6.
- None, otherwise.
"""
if self.ip:
return self.ip.src_addr
@src_addr.setter
def src_addr(self, val):
self.ip.src_addr = val
@property
def dst_addr(self):
"""
- The destination address, if the packet is valid IPv4 or IPv6.
- None, otherwise.
"""
if self.ip:
return self.ip.dst_addr
@dst_addr.setter
def dst_addr(self, val):
self.ip.dst_addr = val
@property
def src_port(self):
"""
- The source port, if the packet is valid TCP or UDP.
- None, otherwise.
"""
if self._port:
return self._port.src_port
@src_port.setter
def src_port(self, val):
self._port.src_port = val
@property
def dst_port(self):
"""
- The destination port, if the packet is valid TCP or UDP.
- None, otherwise.
"""
if self._port:
return self._port.dst_port
@dst_port.setter
def dst_port(self, val):
self._port.dst_port = val
@property
def payload(self):
"""
- The payload, if the packet is valid TCP, UDP, ICMP or ICMPv6.
- None, otherwise.
"""
if self._payload:
return self._payload.payload
@payload.setter
def payload(self, val):
self._payload.payload = val
def recalculate_checksums(self, flags=0):
"""
(Re)calculates the checksum for any IPv4/ICMP/ICMPv6/TCP/UDP checksum present in the given packet.
Individual checksum calculations may be disabled via the appropriate flag.
Typically this function should be invoked on a modified packet before it is injected with WinDivert.send().
Returns the number of checksums calculated.
See: https://reqrypt.org/windivert-doc.html#divert_helper_calc_checksums
"""
buff, buff_ = self.__to_buffers()
num = windivert_dll.WinDivertHelperCalcChecksums(ctypes.byref(buff_), len(self.raw), flags)
if PY2:
self.raw = memoryview(buff)[:len(self.raw)]
return num
def __to_buffers(self):
buff = bytearray(self.raw.tobytes()) if PY2 else self.raw.obj
return buff, (ctypes.c_char * len(self.raw)).from_buffer(buff)
@property
def wd_addr(self):
"""
Gets the interface and direction as a `WINDIVERT_ADDRESS` structure.
:return: The `WINDIVERT_ADDRESS` structure.
"""
address = windivert_dll.WinDivertAddress()
address.IfIdx, address.SubIfIdx = self.interface
address.Direction = self.direction
return address
def matches(self, filter, layer=Layer.NETWORK):
"""
Evaluates the packet against the given packet filter string.
The remapped function is::
BOOL WinDivertHelperEvalFilter(
__in const char *filter,
__in WINDIVERT_LAYER layer,
__in PVOID pPacket,
__in UINT packetLen,
__in PWINDIVERT_ADDRESS pAddr
);
See: https://reqrypt.org/windivert-doc.html#divert_helper_eval_filter
:param filter: The filter string.
:param layer: The network layer.
:return: True if the packet matches, and False otherwise.
"""
buff, buff_ = self.__to_buffers()
return windivert_dll.WinDivertHelperEvalFilter(filter.encode(), layer, ctypes.byref(buff_), len(self.raw),
ctypes.byref(self.wd_addr))

View File

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import struct
class Header(object):
def __init__(self, packet, start=0):
self._packet = packet # type: "pydivert.Packet"
self._start = start
@property
def raw(self):
"""
The raw header, possibly including payload.
"""
return self._packet.raw[self._start:]
@raw.setter
def raw(self, val):
if len(val) == len(self.raw):
self.raw[:] = val
else:
self._packet.raw = memoryview(bytearray(
self._packet.raw[:self._start].tobytes() + val
))
self._packet.ip.packet_len = len(self._packet.raw)
def __setattr__(self, key, value):
if key in dir(self) or key in {"_packet", "_start"}:
return super(Header, self).__setattr__(key, value)
raise AttributeError("AttributeError: '{}' object has no attribute '{}'".format(
type(self).__name__,
key
))
class PayloadMixin(object):
@property
def header_len(self):
raise NotImplementedError() # pragma: no cover
@property
def payload(self):
"""
The packet payload data.
"""
return self.raw[self.header_len:].tobytes()
@payload.setter
def payload(self, val):
if len(val) == len(self.raw) - self.header_len:
self.raw[self.header_len:] = val
else:
self.raw = self.raw[:self.header_len].tobytes() + val
class PortMixin(object):
@property
def src_port(self):
"""
The source port.
"""
return struct.unpack_from("!H", self.raw, 0)[0]
@property
def dst_port(self):
"""
The destination port.
"""
return struct.unpack_from("!H", self.raw, 2)[0]
@src_port.setter
def src_port(self, val):
self.raw[0:2] = struct.pack("!H", val)
@dst_port.setter
def dst_port(self, val):
self.raw[2:4] = struct.pack("!H", val)

View File

@@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from pydivert.packet.header import Header, PayloadMixin
from pydivert.util import indexbyte as i, raw_property
class ICMPHeader(Header, PayloadMixin):
header_len = 4
@property
def type(self):
"""
The ICMP message type.
"""
return i(self.raw[0])
@type.setter
def type(self, val):
self.raw[0] = i(val)
@property
def code(self):
"""
The ICMP message code.
"""
return i(self.raw[1])
@code.setter
def code(self, val):
self.raw[1] = i(val)
cksum = raw_property('!H', 2, docs='The ICMP header checksum field.')
class ICMPv4Header(ICMPHeader):
pass
class ICMPv6Header(ICMPHeader):
pass

View File

@@ -0,0 +1,216 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import socket
import struct
from pydivert.packet.header import Header
from pydivert.util import PY2, PY34, flag_property, indexbyte as i, raw_property
class IPHeader(Header):
_src_addr = slice(0, 0)
_dst_addr = slice(0, 0)
_af = None
@property
def src_addr(self):
"""
The packet source address.
"""
try:
return socket.inet_ntop(self._af, self.raw[self._src_addr].tobytes())
except (ValueError, socket.error):
pass
@src_addr.setter
def src_addr(self, val):
self.raw[self._src_addr] = socket.inet_pton(self._af, val)
@property
def dst_addr(self):
"""
The packet destination address.
"""
try:
return socket.inet_ntop(self._af, self.raw[self._dst_addr].tobytes())
except (ValueError, socket.error):
pass
@dst_addr.setter
def dst_addr(self, val):
self.raw[self._dst_addr] = socket.inet_pton(self._af, val)
@property
def packet_len(self):
"""
The total packet length, including *all* headers, as reported by the IP header.
"""
raise NotImplementedError() # pragma: no cover
@packet_len.setter
def packet_len(self, val):
raise NotImplementedError() # pragma: no cover
class IPv4Header(IPHeader):
_src_addr = slice(12, 16)
_dst_addr = slice(16, 20)
_af = socket.AF_INET
@property
def header_len(self):
"""
The IP header length in bytes.
"""
return self.hdr_len * 4
@property
def hdr_len(self):
"""
The header length in words of 32bit.
"""
return i(self.raw[0]) & 0x0F
@hdr_len.setter
def hdr_len(self, val):
if val < 5:
raise ValueError("IP header length must be greater or equal than 5.")
struct.pack_into('!B', self.raw, 0, 0x40 | val)
packet_len = raw_property('!H', 2, docs=IPHeader.packet_len.__doc__)
tos = raw_property('!B', 1, docs='The Type Of Service field (six-bit DiffServ field and a two-bit ECN field).')
ident = raw_property('!H', 4, docs='The Identification field.')
reserved = flag_property('reserved', 6, 0b10000000)
evil = flag_property('evil', 6, 0b10000000, docs='Just an april\'s fool joke for the RESERVED flag.')
df = flag_property('df', 6, 0b01000000)
mf = flag_property('mf', 6, 0b00100000)
ttl = raw_property('!B', 8, docs='The Time To Live field.')
protocol = raw_property('!B', 9, docs='The Protocol field.')
cksum = raw_property('!H', 10, docs='The IP header Checksum field.')
@property
def flags(self):
"""
The flags field: RESERVED (the evil bit), DF (don't fragment), MF (more fragments).
"""
return i(self.raw[6]) >> 5
@flags.setter
def flags(self, val):
struct.pack_into('!B', self.raw, 6, (val << 5) | (self.frag_offset & 0xFF00))
@property
def frag_offset(self):
"""
The Fragment Offset field in blocks of 8 bytes.
"""
return struct.unpack_from("!H", self.raw, 6)[0] & 0x1FFF
@frag_offset.setter
def frag_offset(self, val):
self.raw[6:8] = struct.pack("!H", (self.flags << 13) | (val & 0x1FFF))
@property
def dscp(self):
"""
The Differentiated Services Code Point field (originally defined as Type of Service) also known as DiffServ.
"""
return (i(self.raw[1]) >> 2) & 0x3F
@dscp.setter
def dscp(self, val):
struct.pack_into('!B', self.raw, 1, (val << 2) | self.ecn)
diff_serv = dscp
@property
def ecn(self):
"""
The Explicit Congestion Notification field.
"""
return i(self.raw[1]) & 0x03
@ecn.setter
def ecn(self, val):
struct.pack_into('!B', self.raw, 1, (self.dscp << 2) | (val & 0x03))
class IPv6Header(IPHeader):
_src_addr = slice(8, 24)
_dst_addr = slice(24, 40)
_af = socket.AF_INET6
header_len = 40
payload_len = raw_property('!H', 4, docs='The Payload Length field.')
next_hdr = raw_property('!B', 6, docs='The Next Header field. Replaces the Protocol field in IPv4.')
hop_limit = raw_property('!B', 7, docs='The Hop Limit field. Replaces the TTL field in IPv4.')
@property
def packet_len(self):
return self.payload_len + self.header_len
@packet_len.setter
def packet_len(self, val):
self.payload_len = val - self.header_len
@property
def traffic_class(self):
"""
The Traffic Class field (six-bit DiffServ field and a two-bit ECN field).
"""
return (struct.unpack_from('!H', self.raw, 0)[0] >> 4) & 0x00FF
@traffic_class.setter
def traffic_class(self, val):
struct.pack_into('!H', self.raw, 0, 0x6000 | (val << 4) | (self.flow_label & 0x000F0000))
@property
def flow_label(self):
"""
The Flow Label field.
"""
return struct.unpack_from('!I', self.raw, 0)[0] & 0x000FFFFF
@flow_label.setter
def flow_label(self, val):
struct.pack_into('!I', self.raw, 0, 0x60000000 | (self.traffic_class << 20) | (val & 0x000FFFFF))
@property
def diff_serv(self):
"""
The DiffServ field.
"""
return (self.traffic_class & 0xFC) >> 2
@diff_serv.setter
def diff_serv(self, val):
self.traffic_class = self.ecn | (val << 2)
@property
def ecn(self):
"""
The Explicit Congestion Notification field.
"""
return self.traffic_class & 0x03
@ecn.setter
def ecn(self, val):
self.traffic_class = (self.diff_serv << 2) | val
if not PY2 and not PY34:
packet_len.__doc__ = IPHeader.packet_len.__doc__

View File

@@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import struct
from pydivert.packet.header import Header, PayloadMixin, PortMixin
from pydivert.util import indexbyte as i, flag_property, raw_property
class TCPHeader(Header, PayloadMixin, PortMixin):
ns = flag_property("ns", 12, 0b00000001)
cwr = flag_property("cwr", 13, 0b10000000)
ece = flag_property("ece", 13, 0b01000000)
urg = flag_property("syn", 13, 0b00100000)
ack = flag_property("ack", 13, 0b00010000)
psh = flag_property("psh", 13, 0b00001000)
rst = flag_property("rst", 13, 0b00000100)
syn = flag_property("syn", 13, 0b00000010)
fin = flag_property("fin", 13, 0b00000001)
@property
def header_len(self):
"""
The TCP header length.
"""
return self.data_offset * 4
seq_num = raw_property('!I', 4, docs='The sequence number field.')
ack_num = raw_property('!I', 8, docs='The acknowledgement number field.')
window_size = raw_property('!H', 14, docs='The size of the receive window in bytes.')
cksum = raw_property('!H', 16, docs='The TCP header checksum field.')
urg_ptr = raw_property('!H', 18, docs='The Urgent Pointer field.')
@property
def data_offset(self):
"""
The size of TCP header in 32bit words.
"""
return i(self.raw[12]) >> 4
@data_offset.setter
def data_offset(self, val):
if val < 5 or val > 15:
raise ValueError("TCP data offset must be greater or equal than 5 and less than 15.")
struct.pack_into('!B', self.raw, 12, (val << 4) | (self.reserved << 1) | self.ns)
@property
def reserved(self):
"""
The reserved field.
"""
return (i(self.raw[12]) >> 1) & 0x07
@reserved.setter
def reserved(self, val):
struct.pack_into('!B', self.raw, 12, (self.data_offset << 4) | (val << 1) | self.ns)
@property
def control_bits(self):
"""
The Control Bits field.
"""
return struct.unpack_from('!H', self.raw, 12)[0] & 0x01FF
@control_bits.setter
def control_bits(self, val):
struct.pack_into('!H', self.raw, 12, (self.data_offset << 12) | (self.reserved << 9) | (val & 0x01FF))

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import struct
from pydivert.packet.header import Header, PayloadMixin, PortMixin
from pydivert.util import PY2, PY34, raw_property
class UDPHeader(Header, PayloadMixin, PortMixin):
header_len = 8
@property
def payload(self):
return PayloadMixin.payload.fget(self)
@payload.setter
def payload(self, val):
PayloadMixin.payload.fset(self, val)
self.payload_len = len(val)
if not PY2 and not PY34:
payload.__doc__ = PayloadMixin.payload.__doc__
@property
def payload_len(self):
return struct.unpack_from("!H", self.raw, 4)[0] - 8
@payload_len.setter
def payload_len(self, val):
self.raw[4:6] = struct.pack("!H", val + 8)
cksum = raw_property('!H', 6, docs='The UDP header checksum field.')

View File

@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import itertools
import socket
import threading
import pydivert
import pytest
try:
from queue import Queue
except ImportError:
from Queue import Queue
@pytest.fixture
def windivert_handle():
with pydivert.WinDivert("false") as w:
yield w
@pytest.fixture(params=list(itertools.product(
("ipv4", "ipv6"),
("tcp", "udp"),
)), ids=lambda x: ",".join(x))
def scenario(request):
ip_version, proto = request.param
if ip_version == "ipv4":
atype = socket.AF_INET
host = "127.0.0.1"
else:
atype = socket.AF_INET6
host = "::1"
if proto == "tcp":
stype = socket.SOCK_STREAM
else:
stype = socket.SOCK_DGRAM
server = socket.socket(atype, stype)
server.bind((host, 0))
client = socket.socket(atype, stype)
client.bind((host, 0))
reply = Queue()
if proto == "tcp":
def server_echo():
server.listen(1)
conn, addr = server.accept()
conn.sendall(conn.recv(4096).upper())
conn.close()
def send(addr, data):
client.connect(addr)
client.sendall(data)
reply.put(client.recv(4096))
else:
def server_echo():
data, addr = server.recvfrom(4096)
server.sendto(data.upper(), addr)
def send(addr, data):
client.sendto(data, addr)
data, recv_addr = client.recvfrom(4096)
assert addr[:2] == recv_addr[:2] # only accept responses from the same host
reply.put(data)
server_thread = threading.Thread(target=server_echo)
server_thread.start()
filt = "{proto}.SrcPort == {c_port} or {proto}.SrcPort == {s_port}".format(
proto=proto,
c_port=client.getsockname()[1],
s_port=server.getsockname()[1]
)
def send_thread(*args, **kwargs):
threading.Thread(target=send, args=args, kwargs=kwargs).start()
return reply
with pydivert.WinDivert(filt) as w:
yield client.getsockname(), server.getsockname(), w, send_thread
client.close()
server.close()

View File

@@ -0,0 +1,517 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import socket
import pydivert
import pytest
from hypothesis import given, example
from hypothesis.strategies import binary
from pydivert import util
from pydivert.consts import Protocol, Direction
def p(raw):
return pydivert.Packet(raw, (0, 0), Direction.OUTBOUND)
ipv4_hdr = util.fromhex("45200028fa8d40002906368b345ad4f0c0a856a4")
ipv6_hdr = util.fromhex("600d684a00280640fc000002000000020000000000000001fc000002000000010000000000000001")
@given(raw=binary(0, 500, 1600))
@example(raw=b'`\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
@example(raw=b'E\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
def test_fuzz(raw):
assert repr(p(raw))
assert repr(p(ipv4_hdr + raw))
assert repr(p(ipv6_hdr + raw))
def test_ipv6_tcp():
raw = util.fromhex("600d684a007d0640fc000002000000020000000000000001fc000002000000010000000000000001a9a01f90021b638"
"dba311e8e801800cfc92e00000101080a801da522801da522474554202f68656c6c6f2e74787420485454502f312e31"
"0d0a557365722d4167656e743a206375726c2f372e33382e300d0a486f73743a205b666330303a323a303a313a3a315"
"d3a383038300d0a4163636570743a202a2f2a0d0a0d0a")
x = p(raw)
assert x.address_family == socket.AF_INET6
assert x.protocol[0] == Protocol.TCP
assert x.src_addr == "fc00:2:0:2::1"
assert x.dst_addr == "fc00:2:0:1::1"
assert x.src_port == 43424
assert x.dst_port == 8080
assert not x.ipv4
assert x.ipv6
assert x.tcp
assert not x.udp
assert not x.icmp
assert x.payload == (
b"GET /hello.txt HTTP/1.1\r\n"
b"User-Agent: curl/7.38.0\r\n"
b"Host: [fc00:2:0:1::1]:8080\r\n"
b"Accept: */*\r\n\r\n"
)
assert x.ip.packet_len == 165
assert repr(x)
def test_ipv4_udp():
raw = util.fromhex("4500004281bf000040112191c0a82b09c0a82b01c9dd0035002ef268528e01000001000000000000013801380138013"
"807696e2d61646472046172706100000c0001")
x = p(raw)
assert x.address_family == socket.AF_INET
assert x.protocol[0] == Protocol.UDP
assert x.src_addr == "192.168.43.9"
assert x.dst_addr == "192.168.43.1"
assert x.src_port == 51677
assert x.dst_port == 53
assert x.ipv4
assert not x.ipv6
assert not x.tcp
assert x.udp
assert not x.icmp
assert x.payload == util.fromhex("528e01000001000000000000013801380138013807696e2d61646472046172706100000c0001")
assert x.udp.payload_len == 38
assert repr(x)
def test_icmp_ping():
raw = util.fromhex("4500005426ef0000400157f9c0a82b09080808080800bbb3d73b000051a7d67d000451e408090a0b0c0d0e0f1011121"
"31415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637")
x = p(raw)
assert x.address_family == socket.AF_INET
assert x.protocol[0] == Protocol.ICMP
assert x.src_addr == "192.168.43.9"
assert x.dst_addr == "8.8.8.8"
assert x.src_port is None
assert x.dst_port is None
assert x.icmp.type == 8
assert x.icmp.code == 0
assert x.ipv4
assert not x.ipv6
assert not x.tcp
assert not x.udp
assert x.icmpv4
assert not x.icmpv6
assert x.payload == util.fromhex("d73b000051a7d67d000451e408090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232"
"425262728292a2b2c2d2e2f3031323334353637")
assert repr(x)
def test_icmpv6_unreachable():
raw = util.fromhex("6000000000443a3d3ffe05010410000002c0dffffe47033e3ffe050700000001020086fffe0580da010413520000000"
"060000000001411013ffe050700000001020086fffe0580da3ffe05010410000002c0dffffe47033ea07582a40014cf"
"470a040000f9c8e7369d250b00")
x = p(raw)
assert x.address_family == socket.AF_INET6
assert x.protocol[0] == Protocol.ICMPV6
assert x.src_addr == "3ffe:501:410:0:2c0:dfff:fe47:33e"
assert x.dst_addr == "3ffe:507:0:1:200:86ff:fe05:80da"
assert x.src_port is None
assert x.dst_port is None
assert x.icmp.type == 1
assert x.icmp.code == 4
assert not x.ipv4
assert x.ipv6
assert not x.tcp
assert not x.udp
assert not x.icmpv4
assert x.icmpv6
assert x.payload == util.fromhex("0000000060000000001411013ffe050700000001020086fffe0580da3ffe05010410000002c0dffff"
"e47033ea07582a40014cf470a040000f9c8e7369d250b00")
assert repr(x)
def test_ipv4_tcp_modify():
raw = util.fromhex("45000051476040008006f005c0a856a936f274fdd84201bb0876cfd0c19f9320501800ff8dba0000170303002400000"
"00000000c2f53831a37ed3c3a632f47440594cab95283b558bf82cb7784344c3314")
x = p(raw)
assert x.protocol[0] == Protocol.TCP
# src_addr
x.src_addr = "1.2.3.4"
with pytest.raises(Exception):
x.src_addr = "::1"
with pytest.raises(Exception):
x.src_addr = 42
assert x.src_addr == "1.2.3.4"
# dst_addr
x.dst_addr = "4.3.2.1"
with pytest.raises(Exception):
x.dst_addr = "::1"
assert x.dst_addr == "4.3.2.1"
# src_port
x.src_port = 42
with pytest.raises(Exception):
x.src_port = "bogus"
assert x.src_port == 42
# dst_port
x.dst_port = 43
with pytest.raises(Exception):
x.dst_port = "bogus"
assert x.dst_port == 43
# tcp_ack (others follow trivially)
x.tcp.ack = False
assert x.tcp.ack is False
x.tcp.ack = True
assert x.tcp.ack is True
# payload
x.payload = b"test"
with pytest.raises(Exception):
x.payload = 42
assert x.payload == b"test"
# checksum
a = x.raw.tobytes()
assert x.recalculate_checksums(
pydivert.CalcChecksumsOption.NO_IP_CHECKSUM |
pydivert.CalcChecksumsOption.NO_TCP_CHECKSUM
) == 0
assert x.raw.tobytes() == a
assert x.recalculate_checksums() == 2
assert x.raw.tobytes() != a
# test same length raw replace.
x.tcp.raw = x.tcp.raw.tobytes().replace(b"test", b"abcd")
# catch typo in headers
with pytest.raises(AttributeError):
x.tcp.typo = 42
def test_ipv6_udp_modify():
raw = util.fromhex("60000000002711403ffe050700000001020086fffe0580da3ffe0501481900000000000000000042095d0035002746b"
"700060100000100000000000003777777057961686f6f03636f6d00000f0001")
x = p(raw)
assert x.protocol[0] == Protocol.UDP
# src_addr
x.src_addr = "::1"
with pytest.raises(Exception):
x.src_addr = "127.0.0.1"
with pytest.raises(Exception):
x.src_addr = 42
assert x.src_addr == "::1"
# dst_addr
x.dst_addr = "::2"
with pytest.raises(Exception):
x.dst_addr = "bogus"
assert x.dst_addr == "::2"
# src_port
x.src_port = 42
with pytest.raises(Exception):
x.src_port = "bogus"
assert x.src_port == 42
# dst_port
x.dst_port = 43
with pytest.raises(Exception):
x.dst_port = "bogus"
assert x.dst_port == 43
# payload
x.payload = b"test"
with pytest.raises(Exception):
x.payload = 42
assert x.payload == b"test"
# checksum
a = x.raw.tobytes()
assert x.recalculate_checksums(
pydivert.CalcChecksumsOption.NO_IP_CHECKSUM |
pydivert.CalcChecksumsOption.NO_UDP_CHECKSUM
) == 0
assert x.raw.tobytes() == a
assert x.recalculate_checksums() == 1
assert x.raw.tobytes() != a
def test_icmp_modify():
raw = util.fromhex("4500005426ef0000400157f9c0a82b09080808080800bbb3d73b000051a7d67d000451e408090a0b0c0d0e0f1011121"
"31415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637")
x = p(raw)
assert x.protocol[0] == Protocol.ICMP
# src_addr
x.src_addr = "1.2.3.4"
with pytest.raises(Exception):
x.src_addr = "::1"
with pytest.raises(Exception):
x.src_addr = 42
assert x.src_addr == "1.2.3.4"
# dst_addr
x.dst_addr = "4.3.2.1"
with pytest.raises(Exception):
x.dst_addr = "::1"
assert x.dst_addr == "4.3.2.1"
# payload
x.payload = b"test"
with pytest.raises(Exception):
x.payload = 42
assert x.payload == b"test"
# icmp
x.icmp.type = 42
with pytest.raises(Exception):
x.icmp.type = "bogus"
assert x.icmp.type == 42
x.icmp.code = 42
with pytest.raises(Exception):
x.icmp.code = "bogus"
assert x.icmp.code == 42
# checksum
a = x.raw.tobytes()
assert x.recalculate_checksums(
pydivert.CalcChecksumsOption.NO_IP_CHECKSUM |
pydivert.CalcChecksumsOption.NO_ICMP_CHECKSUM
) == 0
assert x.raw.tobytes() == a
assert x.recalculate_checksums() == 2
assert x.raw.tobytes() != a
def test_meta():
p = pydivert.Packet(b"", (1, 1), Direction.OUTBOUND)
assert p.is_outbound
assert not p.is_inbound
assert p.is_loopback
p2 = pydivert.Packet(b"", (2, 2), Direction.INBOUND)
assert not p2.is_outbound
assert p2.is_inbound
assert not p2.is_loopback
def test_bogus():
x = p(b"")
with pytest.raises(Exception):
x.src_addr = "127.0.0.1"
with pytest.raises(Exception):
x.dst_addr = "127.0.0.1"
with pytest.raises(Exception):
x.src_port = 80
with pytest.raises(Exception):
x.dst_port = 80
with pytest.raises(Exception):
x.payload = b""
with pytest.raises(Exception):
x.icmp.code = 42
with pytest.raises(Exception):
x.tcp.ack = True
with pytest.raises(Exception):
x.tcp.unknown_attr = True
assert x.recalculate_checksums() == 0
def test_ipv6_extension_headers():
# AH Header
raw = util.fromhex("6e000000003c3301fe800000000000000000000000000001ff020000000000000000000000000005590400000000010"
"00000001321d3a95c5ffd4d184622b9f8030100240101010100000001fb8600000000000501000013000a0028000000"
"0000000000")
assert p(raw).protocol[0] == 89
# Fragmented...
raw = util.fromhex("6000000005b02c80fe8000000000000002105afffeaa20a2fe800000000000000250dafffed8c1533a0000010000000"
"580009e9d0000000d6162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70"
"717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717"
"273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273"
"747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747"
"576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576"
"776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776"
"162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162"
"636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636"
"465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465"
"666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666"
"768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768"
"696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696"
"a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b"
"6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6"
"d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e"
"6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7"
"0717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071"
"7273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727"
"3747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727374"
"7576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727374757"
"6776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727374757677"
"6162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727374757677616"
"2636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727374757677616263"
"6465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727374757677616263646"
"5666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727374757677616263646566"
"6768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727374757677616263646566676"
"8696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f7071727374757677616263646566676869"
"6a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6"
"b6c6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c"
"6d6e6f70717273747576776162636465666768696a6b6c6d6e6f70717273747576776162636465666768696a6b6c6d6"
"e6f70717273747576776162636465666768696a6b6c6d6e")
assert p(raw).protocol[0] == Protocol.ICMPV6
# HOPOPTS
raw = util.fromhex("600000000020000100000000000000000000000000000000ff0200000000000000000000000000013a0005020000000"
"082007ac103e8000000000000000000000000000000000000")
assert p(raw).protocol[0] == Protocol.ICMPV6
def test_ipv4_fields():
raw = util.fromhex("4500005426ef0000400157f9c0a82b09080808080800bbb3d73b000051a7d67d000451e408090a0b0c0d0e0f1011121"
"31415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637")
ip = p(raw).ipv4
assert not ip.df
ip.df = True
assert ip.df
assert ip.flags == 2
assert ip.frag_offset == 0
ip.flags = 3
assert ip.flags == 3
assert ip.mf
assert ip.df
assert ip.frag_offset == 0
ip.ecn = 3
assert ip.ecn == 3
ip.dscp = 18
assert ip.dscp == 18
assert ip.diff_serv == ip.dscp
assert ip.ecn == 3
assert ip.tos == 75
ip.tos = 1
assert ip.tos == 1
assert ip.ecn == 1
assert ip.dscp == 0
ip.flags = 1
assert ip.mf
ip.mf = False
assert not ip.mf
assert ip.flags == 0
ip.frag_offset = 65
assert ip.frag_offset == 65
assert ip.flags == 0
ip.flags = 7
assert ip.frag_offset == 65
assert ip.evil
assert ip.reserved == ip.evil
ip.evil = False
assert not ip.evil
assert ip.reserved == ip.evil
assert ip.flags == 3
ip.ident = 257
assert ip.ident == 257
assert ip.hdr_len == 5
ip.cksum = 514
assert ip.cksum == 514
ip.hdr_len = 6
assert ip.hdr_len == 6
assert ip.header_len == 6 * 4
ip.ttl = 4
assert ip.ttl == 4
ip.protocol = Protocol.FRAGMENT
assert ip.protocol == Protocol.FRAGMENT
with pytest.raises(ValueError):
ip.hdr_len = 4
def test_ipv6_fields():
raw = util.fromhex("6e000000003c3301fe800000000000000000000000000001ff020000000000000000000000000005590400000000010"
"00000001321d3a95c5ffd4d184622b9f8030100240101010100000001fb8600000000000501000013000a0028000000"
"0000000000")
ip = p(raw).ipv6
ip.traffic_class = 3
assert ip.traffic_class == 3
assert ip.ecn == 3
ip.ecn = 0
assert ip.ecn == 0
assert ip.traffic_class == 0
ip.diff_serv = 8
assert ip.diff_serv == 8
assert ip.traffic_class == 32
ip.flow_label = 17
assert ip.flow_label == 17
assert ip.traffic_class == 32
def test_icmp_fields():
raw = util.fromhex("4500005426ef0000400157f9c0a82b09080808080800bbb3d73b000051a7d67d000451e408090a0b0c0d0e0f1011121"
"31415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637")
icmp = p(raw).icmp
icmp.cksum = 11
assert icmp.cksum == 11
def test_tcp_fields():
raw = util.fromhex("45000051476040008006f005c0a856a936f274fdd84201bb0876cfd0c19f9320501800ff8dba0000170303002400000"
"00000000c2f53831a37ed3c3a632f47440594cab95283b558bf82cb7784344c3314")
tcp = p(raw).tcp
assert tcp.reserved == 0
tcp.reserved = 7
assert tcp.reserved == 7
assert not tcp.ns
tcp.ns = True
assert tcp.ns
assert tcp.reserved == 0b111
assert tcp.header_len == tcp.data_offset * 4
tcp.data_offset = 5
assert tcp.data_offset == 5
with pytest.raises(ValueError):
tcp.data_offset = 4
with pytest.raises(ValueError):
tcp.data_offset = 16
tcp.cwr = True
assert tcp.cwr
tcp.ece = True
assert tcp.ece
tcp.syn = True
tcp.control_bits = 0x01F0
assert not tcp.fin
assert not tcp.syn
assert tcp.control_bits == 0x01F0
assert tcp.ece
assert tcp.ns
tcp.ns = False
assert tcp.control_bits == 0x00F0
def test_udp_fields():
raw = util.fromhex("4500004281bf000040112191c0a82b09c0a82b01c9dd0035002ef268528e01000001000000000000013801380138013"
"807696e2d61646472046172706100000c0001")
udp = p(raw).udp
udp.cksum = 0xAAAA
assert udp.cksum == 0xAAAA
def test_filter_match():
raw = util.fromhex("4500004281bf000040112191c0a82b09c0a82b01c9dd0035002ef268528e01000001000000000000013801380138013"
"807696e2d61646472046172706100000c0001")
p = pydivert.Packet(raw, (1, 1), Direction.OUTBOUND)
assert p.matches("true")
assert p.matches("udp and outbound")
assert not p.matches("tcp")

View File

@@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import time
import pytest
from pydivert.consts import Param
from pydivert.windivert import WinDivert
from .fixtures import scenario, windivert_handle as w
assert scenario, w # keep fixtures
def test_open():
w = WinDivert("false")
w.open()
assert w.is_open
w.close()
assert not w.is_open
with w:
# open a second one.
with WinDivert("false") as w2:
assert w2.is_open
assert w.is_open
assert "open" in repr(w)
with pytest.raises(RuntimeError):
w.open()
assert not w.is_open
assert "closed" in repr(w)
with pytest.raises(RuntimeError):
w.recv()
with pytest.raises(RuntimeError):
w.close()
def test_register():
if WinDivert.is_registered():
WinDivert.unregister()
while WinDivert.is_registered():
time.sleep(0.01) # pragma: no cover
assert not WinDivert.is_registered()
WinDivert.register()
assert WinDivert.is_registered()
def test_unregister():
w = WinDivert("false")
w.open()
WinDivert.unregister()
time.sleep(0.1)
assert WinDivert.is_registered()
w.close()
# may not trigger immediately.
while WinDivert.is_registered():
time.sleep(0.01) # pragma: no cover
class TestParams(object):
def test_queue_time_range(self, w):
"""
Tests setting the minimum value for queue time.
From docs: 128 < default 512 < 2048
"""
def_range = (128, 512, 2048)
for value in def_range:
w.set_param(Param.QUEUE_TIME, value)
assert value == w.get_param(Param.QUEUE_TIME)
def test_queue_len_range(self, w):
"""
Tests setting the minimum value for queue length.
From docs: 1< default 512 <8192
"""
for value in (1, 512, 8192):
w.set_param(Param.QUEUE_LEN, value)
assert value == w.get_param(Param.QUEUE_LEN)
def test_invalid_set(self, w):
with pytest.raises(Exception):
w.set_param(42, 43)
def test_invalid_get(self, w):
with pytest.raises(Exception):
w.get_param(42)
def test_echo(scenario):
client_addr, server_addr, w, send = scenario
w = w # type: WinDivert
reply = send(server_addr, b"echo")
for p in w:
assert p.is_loopback
assert p.is_outbound
w.send(p)
done = (
p.udp and p.dst_port == client_addr[1]
or
p.tcp and p.tcp.fin
)
if done:
break
assert reply.get() == b"ECHO"
def test_divert(scenario):
client_addr, server_addr, w, send = scenario
w = w # type: WinDivert
target = (server_addr[0], 80)
reply = send(target, b"echo")
for p in w:
if p.src_port == client_addr[1]:
p.dst_port = server_addr[1]
if p.src_port == server_addr[1]:
p.src_port = target[1]
w.send(p)
done = (
p.udp and p.dst_port == client_addr[1]
or
p.tcp and p.tcp.fin
)
if done:
break
assert reply.get() == b"ECHO"
def test_modify_payload(scenario):
client_addr, server_addr, w, send = scenario
w = w # type: WinDivert
reply = send(server_addr, b"echo")
for p in w:
p.payload = p.payload.replace(b"echo", b"test").replace(b"TEST", b"ECHO")
w.send(p)
done = (
p.udp and p.dst_port == client_addr[1]
or
p.tcp and p.tcp.fin
)
if done:
break
assert reply.get() == b"ECHO"
def test_packet_cutoff(scenario):
client_addr, server_addr, w, send = scenario
w = w # type: WinDivert
reply = send(server_addr, b"a" * 1000)
cutoff = None
while True:
p = w.recv(500)
if p.ip.packet_len != len(p.raw):
assert cutoff is None
cutoff = p.ip.packet_len - len(p.raw)
p.ip.packet_len = len(p.raw) # fix length
if p.udp:
p.udp.payload_len = len(p.payload)
w.send(p)
done = (
p.udp and p.dst_port == client_addr[1]
or
p.tcp and p.tcp.fin
)
if done:
break
assert cutoff
assert reply.get() == b"A" * (1000 - cutoff)
def test_check_filter():
res, pos, msg = WinDivert.check_filter('true')
assert res
assert pos == 0
assert msg is not None
res, pos, msg = WinDivert.check_filter('something wrong here')
assert not res
assert pos == 0
assert msg is not None
res, pos, msg = WinDivert.check_filter('outbound and something wrong here')
assert not res
assert pos == 13

View File

@@ -0,0 +1,90 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import struct
import sys
class cached_property(object):
"""
A property that is only computed once per instance and then replaces itself
with an ordinary attribute. Deleting the attribute resets the property.
Source: https://github.com/bottlepy/bottle/commit/fa7733e075da0d790d809aa3d2f53071897e6f76
"""
def __init__(self, func):
self.__doc__ = getattr(func, '__doc__')
self.func = func
def __get__(self, obj, cls):
if obj is None: # pragma: no cover
return self
value = obj.__dict__[self.func.__name__] = self.func(obj)
return value
if sys.version_info < (3, 0):
# python 3's byte indexing: b"AAA"[1] == 65
indexbyte = lambda x: chr(x) if isinstance(x, int) else ord(x)
# python 3's bytes.fromhex()
fromhex = lambda x: x.decode("hex")
PY2 = True
PY34 = False
else:
indexbyte = lambda x: x
fromhex = lambda x: bytes.fromhex(x)
PY2 = False
if sys.version_info < (3, 5):
# __doc__ attribute is only writable from 3.5.
PY34 = True
else:
PY34 = False
def flag_property(name, offset, bit, docs=None):
@property
def flag(self):
return bool(indexbyte(self.raw[offset]) & bit)
@flag.setter
def flag(self, val):
flags = indexbyte(self.raw[offset])
if val:
flags |= bit
else:
flags &= ~bit
self.raw[offset] = indexbyte(flags)
if not PY2 and not PY34:
flag.__doc__ = """
Indicates if the {} flag is set.
""".format(name.upper()) if not docs else docs
return flag
def raw_property(fmt, offset, docs=None):
@property
def rprop(self):
return struct.unpack_from(fmt, self.raw, offset)[0]
@rprop.setter
def rprop(self, val):
struct.pack_into(fmt, self.raw, offset, val)
if docs and not PY2 and not PY34:
rprop.__doc__ = docs
return rprop

View File

@@ -0,0 +1,274 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import subprocess
import sys
from ctypes import byref, c_uint64, c_uint, c_char, c_char_p
from pydivert import windivert_dll
from pydivert.consts import Layer, Direction, Flag
from pydivert.packet import Packet
from pydivert.util import PY2
DEFAULT_PACKET_BUFFER_SIZE = 1500
class WinDivert(object):
"""
A WinDivert handle that can be used to capture packets.
The main methods are `.open()`, `.recv()`, `.send()` and `.close()`.
Use it like so::
with pydivert.WinDivert() as w:
for packet in w:
print(packet)
w.send(packet)
"""
def __init__(self, filter="true", layer=Layer.NETWORK, priority=0, flags=Flag.DEFAULT):
self._handle = None
self._filter = filter.encode()
self._layer = layer
self._priority = priority
self._flags = flags
def __repr__(self):
return '<WinDivert state="{}" filter="{}" layer="{}" priority="{}" flags="{}" />'.format(
"open" if self._handle is not None else "closed",
self._filter.decode(),
self._layer,
self._priority,
self._flags
)
def __enter__(self):
self.open()
return self
def __exit__(self, *args):
self.close()
def __iter__(self):
return self
def __next__(self):
return self.recv()
if sys.version_info < (3, 0):
next = __next__
@staticmethod
def register():
"""
An utility method to register the service the first time.
It is usually not required to call this function, as WinDivert will register itself when opening a handle.
"""
with WinDivert("false"):
pass
@staticmethod
def is_registered():
"""
Check if the WinDivert service is currently installed on the system.
"""
return subprocess.call("sc query WinDivert1.3", stdout=subprocess.PIPE,
stderr=subprocess.PIPE) == 0
@staticmethod
def unregister():
"""
Unregisters the WinDivert service.
This function only requests a service stop, which may not be processed immediately if there are still open
handles.
"""
subprocess.check_call("sc stop WinDivert1.3", stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
@staticmethod
def check_filter(filter, layer=Layer.NETWORK):
"""
Checks if the given packet filter string is valid with respect to the filter language.
The remapped function is WinDivertHelperCheckFilter::
BOOL WinDivertHelperCheckFilter(
__in const char *filter,
__in WINDIVERT_LAYER layer,
__out_opt const char **errorStr,
__out_opt UINT *errorPos
);
See: https://reqrypt.org/windivert-doc.html#divert_helper_check_filter
:return: A tuple (res, pos, msg) with check result in 'res' human readable description of the error in 'msg' and the error's position in 'pos'.
"""
res, pos, msg = False, c_uint(), c_char_p()
try:
res = windivert_dll.WinDivertHelperCheckFilter(filter.encode(), layer, byref(msg), byref(pos))
except OSError:
pass
return res, pos.value, msg.value.decode()
def open(self):
"""
Opens a WinDivert handle for the given filter.
Unless otherwise specified by flags, any packet that matches the filter will be diverted to the handle.
Diverted packets can be read by the application with receive().
The remapped function is WinDivertOpen::
HANDLE WinDivertOpen(
__in const char *filter,
__in WINDIVERT_LAYER layer,
__in INT16 priority,
__in UINT64 flags
);
For more info on the C call visit: http://reqrypt.org/windivert-doc.html#divert_open
"""
if self.is_open:
raise RuntimeError("WinDivert handle is already open.")
self._handle = windivert_dll.WinDivertOpen(self._filter, self._layer, self._priority,
self._flags)
@property
def is_open(self):
"""
Indicates if there is currently an open handle.
"""
return bool(self._handle)
def close(self):
"""
Closes the handle opened by open().
The remapped function is WinDivertClose::
BOOL WinDivertClose(
__in HANDLE handle
);
For more info on the C call visit: http://reqrypt.org/windivert-doc.html#divert_close
"""
if not self.is_open:
raise RuntimeError("WinDivert handle is not open.")
windivert_dll.WinDivertClose(self._handle)
self._handle = None
def recv(self, bufsize=DEFAULT_PACKET_BUFFER_SIZE):
"""
Receives a diverted packet that matched the filter.
The remapped function is WinDivertRecv::
BOOL WinDivertRecv(
__in HANDLE handle,
__out PVOID pPacket,
__in UINT packetLen,
__out_opt PWINDIVERT_ADDRESS pAddr,
__out_opt UINT *recvLen
);
For more info on the C call visit: http://reqrypt.org/windivert-doc.html#divert_recv
:return: The return value is a `pydivert.Packet`.
"""
if self._handle is None:
raise RuntimeError("WinDivert handle is not open")
packet = bytearray(bufsize)
packet_ = (c_char * bufsize).from_buffer(packet)
address = windivert_dll.WinDivertAddress()
recv_len = c_uint(0)
windivert_dll.WinDivertRecv(self._handle, packet_, bufsize, byref(address), byref(recv_len))
return Packet(
memoryview(packet)[:recv_len.value],
(address.IfIdx, address.SubIfIdx),
Direction(address.Direction)
)
def send(self, packet, recalculate_checksum=True):
"""
Injects a packet into the network stack.
Recalculates the checksum before sending unless recalculate_checksum=False is passed.
The injected packet may be one received from recv(), or a modified version, or a completely new packet.
Injected packets can be captured and diverted again by other WinDivert handles with lower priorities.
The remapped function is WinDivertSend::
BOOL WinDivertSend(
__in HANDLE handle,
__in PVOID pPacket,
__in UINT packetLen,
__in PWINDIVERT_ADDRESS pAddr,
__out_opt UINT *sendLen
);
For more info on the C call visit: http://reqrypt.org/windivert-doc.html#divert_send
:return: The return value is the number of bytes actually sent.
"""
if recalculate_checksum:
packet.recalculate_checksums()
send_len = c_uint(0)
if PY2:
# .from_buffer(memoryview) does not work on PY2
buff = bytearray(packet.raw)
else:
buff = packet.raw
buff = (c_char * len(packet.raw)).from_buffer(buff)
windivert_dll.WinDivertSend(self._handle, buff, len(packet.raw), byref(packet.wd_addr),
byref(send_len))
return send_len
def get_param(self, name):
"""
Get a WinDivert parameter. See pydivert.Param for the list of parameters.
The remapped function is WinDivertGetParam::
BOOL WinDivertGetParam(
__in HANDLE handle,
__in WINDIVERT_PARAM param,
__out UINT64 *pValue
);
For more info on the C call visit: http://reqrypt.org/windivert-doc.html#divert_get_param
:return: The parameter value.
"""
value = c_uint64(0)
windivert_dll.WinDivertGetParam(self._handle, name, byref(value))
return value.value
def set_param(self, name, value):
"""
Set a WinDivert parameter. See pydivert.Param for the list of parameters.
The remapped function is DivertSetParam::
BOOL WinDivertSetParam(
__in HANDLE handle,
__in WINDIVERT_PARAM param,
__in UINT64 value
);
For more info on the C call visit: http://reqrypt.org/windivert-doc.html#divert_set_param
"""
return windivert_dll.WinDivertSetParam(self._handle, name, value)

View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
pydivert bundles the WinDivert binaries from
https://reqrypt.org/download/WinDivert-1.3.0-WDDK.zip
"""
import functools
import os
import platform
import sys
from ctypes import (
POINTER, GetLastError, WinError, c_uint, c_void_p, c_uint32, c_char_p, ARRAY, c_uint64, c_int16, c_int, WinDLL,
c_uint8, windll)
from ctypes.wintypes import HANDLE
from .structs import WinDivertAddress
ERROR_IO_PENDING = 997
here = os.path.abspath(os.path.dirname(__file__))
if platform.architecture()[0] == "64bit":
DLL_PATH = os.path.join(here, "WinDivert64.dll")
else:
DLL_PATH = os.path.join(here, "WinDivert32.dll")
def raise_on_error(f):
"""
This decorator throws a WinError whenever GetLastError() returns an error.
As as special case, ERROR_IO_PENDING is ignored.
"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
result = f(*args, **kwargs)
retcode = GetLastError()
if retcode and retcode != ERROR_IO_PENDING:
err = WinError(code=retcode)
windll.kernel32.SetLastError(0) # clear error code so that we don't raise twice.
raise err
return result
return wrapper
WINDIVERT_FUNCTIONS = {
"WinDivertHelperParsePacket": [HANDLE, c_uint, c_void_p, c_void_p, c_void_p, c_void_p, c_void_p, c_void_p,
c_void_p, POINTER(c_uint)],
"WinDivertHelperParseIPv4Address": [c_char_p, POINTER(c_uint32)],
"WinDivertHelperParseIPv6Address": [c_char_p, POINTER(ARRAY(c_uint8, 16))],
"WinDivertHelperCalcChecksums": [c_void_p, c_uint, c_uint64],
"WinDivertHelperCheckFilter": [c_char_p, c_int, POINTER(c_char_p), POINTER(c_uint)],
"WinDivertHelperEvalFilter": [c_char_p, c_int, c_void_p, c_uint, c_void_p],
"WinDivertOpen": [c_char_p, c_int, c_int16, c_uint64],
"WinDivertRecv": [HANDLE, c_void_p, c_uint, c_void_p, c_void_p],
"WinDivertSend": [HANDLE, c_void_p, c_uint, c_void_p, c_void_p],
"WinDivertRecvEx": [HANDLE, c_void_p, c_uint, c_uint64, c_void_p, c_void_p, c_void_p],
"WinDivertSendEx": [HANDLE, c_void_p, c_uint, c_uint64, c_void_p, c_void_p, c_void_p],
"WinDivertClose": [HANDLE],
"WinDivertGetParam": [HANDLE, c_int, POINTER(c_uint64)],
"WinDivertSetParam": [HANDLE, c_int, c_uint64],
}
_instance = None
def instance():
global _instance
if _instance is None:
_instance = WinDLL(DLL_PATH)
for funcname, argtypes in WINDIVERT_FUNCTIONS.items():
func = getattr(_instance, funcname)
func.argtypes = argtypes
return _instance
# Dark magic happens below.
# On init, windivert_dll.WinDivertOpen is a proxy function that loads the DLL on the first invocation
# and then replaces all existing proxy function with direct handles to the DLL's functions.
_module = sys.modules[__name__]
def _init():
"""
Lazy-load DLL, replace proxy functions with actual ones.
"""
i = instance()
for funcname in WINDIVERT_FUNCTIONS:
func = getattr(i, funcname)
func = raise_on_error(func)
setattr(_module, funcname, func)
def _mkprox(funcname):
"""
Make lazy-init proxy function.
"""
def prox(*args, **kwargs):
_init()
return getattr(_module, funcname)(*args, **kwargs)
return prox
for funcname in WINDIVERT_FUNCTIONS:
setattr(_module, funcname, _mkprox(funcname))

View File

@@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2016 Fabio Falcinelli, Maximilian Hils
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import ctypes
class WinDivertAddress(ctypes.Structure):
"""
Ctypes Structure for WINDIVERT_ADDRESS.
The WINDIVERT_ADDRESS structure represents the "address" of a captured or injected packet.
The address includes the packet's network interfaces and the packet direction.
typedef struct
{
UINT32 IfIdx;
UINT32 SubIfIdx;
UINT8 Direction;
} WINDIVERT_ADDRESS, *PWINDIVERT_ADDRESS;
Fields:
- IfIdx: The interface index on which the packet arrived (for inbound packets),
or is to be sent (for outbound packets).
- SubIfIdx: The sub-interface index for IfIdx.
- Direction: The packet's direction. The possible values are
- WINDIVERT_DIRECTION_OUTBOUND with value 0 for outbound packets.
- WINDIVERT_DIRECTION_INBOUND with value 1 for inbound packets.
"""
_fields_ = [
("IfIdx", ctypes.c_uint32),
("SubIfIdx", ctypes.c_uint32),
("Direction", ctypes.c_uint8),
]