aboutsummaryrefslogtreecommitdiffstats
path: root/libmproxy/platform/windows.py
diff options
context:
space:
mode:
Diffstat (limited to 'libmproxy/platform/windows.py')
-rw-r--r--libmproxy/platform/windows.py432
1 files changed, 0 insertions, 432 deletions
diff --git a/libmproxy/platform/windows.py b/libmproxy/platform/windows.py
deleted file mode 100644
index 9fe04cfa..00000000
--- a/libmproxy/platform/windows.py
+++ /dev/null
@@ -1,432 +0,0 @@
-import configargparse
-import cPickle as pickle
-from ctypes import byref, windll, Structure
-from ctypes.wintypes import DWORD
-import os
-import socket
-import SocketServer
-import struct
-import threading
-import time
-from collections import OrderedDict
-
-from pydivert.windivert import WinDivert
-from pydivert.enum import Direction, Layer, Flag
-
-
-PROXY_API_PORT = 8085
-
-
-class Resolver(object):
-
- def __init__(self):
- TransparentProxy.setup()
- self.socket = None
- self.lock = threading.RLock()
- self._connect()
-
- def _connect(self):
- if self.socket:
- self.socket.close()
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- self.socket.connect(("127.0.0.1", PROXY_API_PORT))
-
- self.wfile = self.socket.makefile('wb')
- self.rfile = self.socket.makefile('rb')
- pickle.dump(os.getpid(), self.wfile)
-
- def original_addr(self, csock):
- client = csock.getpeername()[:2]
- with self.lock:
- try:
- pickle.dump(client, self.wfile)
- self.wfile.flush()
- addr = pickle.load(self.rfile)
- if addr is None:
- raise RuntimeError("Cannot resolve original destination.")
- addr = list(addr)
- addr[0] = str(addr[0])
- addr = tuple(addr)
- return addr
- except (EOFError, socket.error):
- self._connect()
- return self.original_addr(csock)
-
-
-class APIRequestHandler(SocketServer.StreamRequestHandler):
-
- """
- TransparentProxy API: Returns the pickled server address, port tuple
- for each received pickled client address, port tuple.
- """
-
- def handle(self):
- proxifier = self.server.proxifier
- pid = None
- try:
- pid = pickle.load(self.rfile)
- if pid is not None:
- proxifier.trusted_pids.add(pid)
-
- while True:
- client = pickle.load(self.rfile)
- server = proxifier.client_server_map.get(client, None)
- pickle.dump(server, self.wfile)
- self.wfile.flush()
-
- except (EOFError, socket.error):
- proxifier.trusted_pids.discard(pid)
-
-
-class APIServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
-
- def __init__(self, proxifier, *args, **kwargs):
- SocketServer.TCPServer.__init__(self, *args, **kwargs)
- self.proxifier = proxifier
- self.daemon_threads = True
-
-
-# Windows error.h
-ERROR_INSUFFICIENT_BUFFER = 0x7A
-
-
-# http://msdn.microsoft.com/en-us/library/windows/desktop/bb485761(v=vs.85).aspx
-class MIB_TCPROW2(Structure):
- _fields_ = [
- ('dwState', DWORD),
- ('dwLocalAddr', DWORD),
- ('dwLocalPort', DWORD),
- ('dwRemoteAddr', DWORD),
- ('dwRemotePort', DWORD),
- ('dwOwningPid', DWORD),
- ('dwOffloadState', DWORD)
- ]
-
-
-# http://msdn.microsoft.com/en-us/library/windows/desktop/bb485772(v=vs.85).aspx
-def MIB_TCPTABLE2(size):
- class _MIB_TCPTABLE2(Structure):
- _fields_ = [('dwNumEntries', DWORD),
- ('table', MIB_TCPROW2 * size)]
-
- return _MIB_TCPTABLE2()
-
-
-class TransparentProxy(object):
-
- """
- Transparent Windows Proxy for mitmproxy based on WinDivert/PyDivert.
-
- Requires elevated (admin) privileges. Can be started separately by manually running the file.
-
- This module can be used to intercept and redirect all traffic that is forwarded by the user's machine and
- traffic sent from the machine itself.
-
- How it works:
-
- (1) First, we intercept all packages that match our filter (destination port 80 and 443 by default).
- We both consider traffic that is forwarded by the OS (WinDivert's NETWORK_FORWARD layer) as well as traffic
- sent from the local machine (WinDivert's NETWORK layer). In the case of traffic from the local machine, we need to
- distinguish between traffc sent from applications and traffic sent from the proxy. To accomplish this, we use
- Windows' GetTcpTable2 syscall to determine the source application's PID.
-
- For each intercepted package, we
- 1. Store the source -> destination mapping (address and port)
- 2. Remove the package from the network (by not reinjecting it).
- 3. Re-inject the package into the local network stack, but with the destination address changed to the proxy.
-
- (2) Next, the proxy receives the forwarded packet, but does not know the real destination yet (which we overwrote
- with the proxy's address). On Linux, we would now call getsockopt(SO_ORIGINAL_DST), but that unfortunately doesn't
- work on Windows. However, we still have the correct source information. As a workaround, we now access the forward
- module's API (see APIRequestHandler), submit the source information and get the actual destination back (which the
- forward module stored in (1.3)).
-
- (3) The proxy now establish the upstream connection as usual.
-
- (4) Finally, the proxy sends the response back to the client. To make it work, we need to change the packet's source
- address back to the original destination (using the mapping from (1.3)), to which the client believes he is talking
- to.
-
- Limitations:
-
- - No IPv6 support. (Pull Requests welcome)
- - TCP ports do not get re-used simulateously on the client, i.e. the proxy will fail if application X
- connects to example.com and example.org from 192.168.0.42:4242 simultaneously. This could be mitigated by
- introducing unique "meta-addresses" which mitmproxy sees, but this would remove the correct client info from
- mitmproxy.
-
- """
-
- def __init__(self,
- mode="both",
- redirect_ports=(80, 443), custom_filter=None,
- proxy_addr=False, proxy_port=8080,
- api_host="localhost", api_port=PROXY_API_PORT,
- cache_size=65536):
- """
- :param mode: Redirection operation mode: "forward" to only redirect forwarded packets, "local" to only redirect
- packets originating from the local machine, "both" to redirect both.
- :param redirect_ports: if the destination port is in this tuple, the requests are redirected to the proxy.
- :param custom_filter: specify a custom WinDivert filter to select packets that should be intercepted. Overrides
- redirect_ports setting.
- :param proxy_addr: IP address of the proxy (IP within a network, 127.0.0.1 does not work). By default,
- this is detected automatically.
- :param proxy_port: Port the proxy is listenting on.
- :param api_host: Host the forward module API is listening on.
- :param api_port: Port the forward module API is listening on.
- :param cache_size: Maximum number of connection tuples that are stored. Only relevant in very high
- load scenarios.
- """
- if proxy_port in redirect_ports:
- raise ValueError("The proxy port must not be a redirect port.")
-
- if not proxy_addr:
- # Auto-Detect local IP.
- # https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- s.connect(("8.8.8.8", 80))
- proxy_addr = s.getsockname()[0]
- s.close()
-
- self.mode = mode
- self.proxy_addr, self.proxy_port = proxy_addr, proxy_port
- self.connection_cache_size = cache_size
-
- self.client_server_map = OrderedDict()
-
- self.api = APIServer(self, (api_host, api_port), APIRequestHandler)
- self.api_thread = threading.Thread(target=self.api.serve_forever)
- self.api_thread.daemon = True
-
- self.driver = WinDivert()
- self.driver.register()
-
- self.request_filter = custom_filter or " or ".join(
- ("tcp.DstPort == %d" %
- p) for p in redirect_ports)
- self.request_forward_handle = None
- self.request_forward_thread = threading.Thread(
- target=self.request_forward)
- self.request_forward_thread.daemon = True
-
- self.addr_pid_map = dict()
- self.trusted_pids = set()
- self.tcptable2 = MIB_TCPTABLE2(0)
- self.tcptable2_size = DWORD(0)
- self.request_local_handle = None
- self.request_local_thread = threading.Thread(target=self.request_local)
- self.request_local_thread.daemon = True
-
- # The proxy server responds to the client. To the client,
- # this response should look like it has been sent by the real target
- self.response_filter = "outbound and tcp.SrcPort == %d" % proxy_port
- self.response_handle = None
- self.response_thread = threading.Thread(target=self.response)
- self.response_thread.daemon = True
-
- self.icmp_handle = None
-
- @classmethod
- def setup(cls):
- # TODO: Make sure that server can be killed cleanly. That's a bit difficult as we don't have access to
- # controller.should_exit when this is called.
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- server_unavailable = s.connect_ex(("127.0.0.1", PROXY_API_PORT))
- if server_unavailable:
- proxifier = TransparentProxy()
- proxifier.start()
-
- def start(self):
- self.api_thread.start()
-
- # Block all ICMP requests (which are sent on Windows by default).
- # In layman's terms: If we don't do this, our proxy machine tells the client that it can directly connect to the
- # real gateway if they are on the same network.
- self.icmp_handle = self.driver.open_handle(
- filter="icmp",
- layer=Layer.NETWORK,
- flags=Flag.DROP)
-
- self.response_handle = self.driver.open_handle(
- filter=self.response_filter,
- layer=Layer.NETWORK)
- self.response_thread.start()
-
- if self.mode == "forward" or self.mode == "both":
- self.request_forward_handle = self.driver.open_handle(
- filter=self.request_filter,
- layer=Layer.NETWORK_FORWARD)
- self.request_forward_thread.start()
- if self.mode == "local" or self.mode == "both":
- self.request_local_handle = self.driver.open_handle(
- filter=self.request_filter,
- layer=Layer.NETWORK)
- self.request_local_thread.start()
-
- def shutdown(self):
- if self.mode == "local" or self.mode == "both":
- self.request_local_handle.close()
- if self.mode == "forward" or self.mode == "both":
- self.request_forward_handle.close()
-
- self.response_handle.close()
- self.icmp_handle.close()
- self.api.shutdown()
-
- def recv(self, handle):
- """
- Convenience function that receives a packet from the passed handler and handles error codes.
- If the process has been shut down, (None, None) is returned.
- """
- try:
- raw_packet, metadata = handle.recv()
- return self.driver.parse_packet(raw_packet), metadata
- except WindowsError as e:
- if e.winerror == 995:
- return None, None
- else:
- raise
-
- def fetch_pids(self):
- ret = windll.iphlpapi.GetTcpTable2(
- byref(
- self.tcptable2), byref(
- self.tcptable2_size), 0)
- if ret == ERROR_INSUFFICIENT_BUFFER:
- self.tcptable2 = MIB_TCPTABLE2(self.tcptable2_size.value)
- self.fetch_pids()
- elif ret == 0:
- for row in self.tcptable2.table[:self.tcptable2.dwNumEntries]:
- local = (
- socket.inet_ntoa(struct.pack('L', row.dwLocalAddr)),
- socket.htons(row.dwLocalPort)
- )
- self.addr_pid_map[local] = row.dwOwningPid
- else:
- raise RuntimeError("Unknown GetTcpTable2 return code: %s" % ret)
-
- def request_local(self):
- while True:
- packet, metadata = self.recv(self.request_local_handle)
- if not packet:
- return
-
- client = (packet.src_addr, packet.src_port)
-
- if client not in self.addr_pid_map:
- self.fetch_pids()
-
- # If this fails, we most likely have a connection from an external client to
- # a local server on 80/443. In this, case we always want to proxy
- # the request.
- pid = self.addr_pid_map.get(client, None)
-
- if pid not in self.trusted_pids:
- self._request(packet, metadata)
- else:
- self.request_local_handle.send((packet.raw, metadata))
-
- def request_forward(self):
- """
- Redirect packages to the proxy
- """
- while True:
- packet, metadata = self.recv(self.request_forward_handle)
- if not packet:
- return
-
- self._request(packet, metadata)
-
- def _request(self, packet, metadata):
- # print(" * Redirect client -> server to proxy")
- # print("%s:%s -> %s:%s" % (packet.src_addr, packet.src_port, packet.dst_addr, packet.dst_port))
- client = (packet.src_addr, packet.src_port)
- server = (packet.dst_addr, packet.dst_port)
-
- if client in self.client_server_map:
- # Force re-add to mark as "newest" entry in the dict.
- del self.client_server_map[client]
- while len(self.client_server_map) > self.connection_cache_size:
- self.client_server_map.popitem(False)
-
- self.client_server_map[client] = server
-
- packet.dst_addr, packet.dst_port = self.proxy_addr, self.proxy_port
- metadata.direction = Direction.INBOUND
-
- packet = self.driver.update_packet_checksums(packet)
- # Use any handle thats on the NETWORK layer - request_local may be
- # unavailable.
- self.response_handle.send((packet.raw, metadata))
-
- def response(self):
- """
- Spoof source address of packets send from the proxy to the client
- """
- while True:
- packet, metadata = self.recv(self.response_handle)
- if not packet:
- return
-
- # If the proxy responds to the client, let the client believe the target server sent the packets.
- # print(" * Adjust proxy -> client")
- client = (packet.dst_addr, packet.dst_port)
- server = self.client_server_map.get(client, None)
- if server:
- packet.src_addr, packet.src_port = server
- else:
- print("Warning: Previously unseen connection from proxy to %s:%s." % client)
-
- packet = self.driver.update_packet_checksums(packet)
- self.response_handle.send((packet.raw, metadata))
-
-
-if __name__ == "__main__":
- parser = configargparse.ArgumentParser(
- description="Windows Transparent Proxy")
- parser.add_argument(
- '--mode',
- choices=[
- 'forward',
- 'local',
- 'both'],
- default="both",
- help='redirection operation mode: "forward" to only redirect forwarded packets, '
- '"local" to only redirect packets originating from the local machine')
- group = parser.add_mutually_exclusive_group()
- group.add_argument(
- "--redirect-ports",
- nargs="+",
- type=int,
- default=[
- 80,
- 443],
- metavar="80",
- help="ports that should be forwarded to the proxy")
- group.add_argument(
- "--custom-filter",
- default=None,
- metavar="WINDIVERT_FILTER",
- help="Custom WinDivert interception rule.")
- parser.add_argument("--proxy-addr", default=False,
- help="Proxy Server Address")
- parser.add_argument("--proxy-port", type=int, default=8080,
- help="Proxy Server Port")
- parser.add_argument("--api-host", default="localhost",
- help="API hostname to bind to")
- parser.add_argument("--api-port", type=int, default=PROXY_API_PORT,
- help="API port")
- parser.add_argument("--cache-size", type=int, default=65536,
- help="Maximum connection cache size")
- options = parser.parse_args()
- proxy = TransparentProxy(**vars(options))
- proxy.start()
- print(" * Transparent proxy active.")
- print(" Filter: {0}".format(proxy.request_filter))
- try:
- while True:
- time.sleep(1)
- except KeyboardInterrupt:
- print(" * Shutting down...")
- proxy.shutdown()
- print(" * Shut down.")