# MIT License # Copyright (c) 2017 Balazs Bucsay # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys if "packetselector.py" in sys.argv[0]: print("[-] Instead of poking around just try: python xfltreat.py --help") sys.exit(-1) # This is the engine of the whole communication. Every packet that arrives to # the tunnel will be carefully selected. In case the destination IP matches, it # will be redirected (written) to the appropriate client pipe. import threading import os import select import struct import socket #local files import common from client import Client class PacketSelector(threading.Thread): clients = None def __init__(self, tunnel): threading.Thread.__init__(self) self.timeout = 1.0 # seems to be a good value for timeout self.clients = [] self.tunnel = tunnel self._stop = False self.os_type = common.get_os_type() if self.os_type == common.OS_WINDOWS: self.run_ps_mainloop = self.run_windows else: self.run_ps_mainloop = self.run_unix # return client list def get_clients(self): return self.clients # add new client to the client list def add_client(self, client): self.clients.append(client) return # This function is called when a client object has to be replaced. # That could happen when the client connection was reset, or there is a # duplicated config with the same private IP. def replace_client(self, old_client, new_client): if old_client in self.clients: self.clients.remove(old_client) self.clients.append(new_client) try: old_client.get_pipe_w_fd().close() except: pass try: old_client.get_pipe_r_fd().close() except: pass try: socket.close(old_client.get_socket()) except: pass # removing client from the client list def delete_client(self, client): if client in self.clients: if self.os_type == common.OS_WINDOWS: import win32file try: win32file.CloseHandle(client.get_pipe_r()) win32file.CloseHandle(client.get_pipe_w()) except Exception as e: common.internal_print("Remove authenticated client: CloseHandle exception: {0}".format(e), -1) else: try: client.get_pipe_r_fd().close() client.get_pipe_w_fd().close() except Exception as e: common.internal_print("Remove authenticated client: os.close exception: {0}".format(e), -1) client.call_stopfp() self.clients.remove(client) return # This function should run from the point when the framework was started. # It runs as an infinite loop to read the packets off the tunnel. # When an IPv4 packet was found that will be selected and checked whether # it addresses a client in the client list. If a client was found, then the # packet will be written on that pipe. def run(self): return self.run_ps_mainloop() def run_unix(self): rlist = [self.tunnel] wlist = [] xlist = [] while not self._stop: try: readable, writable, exceptional = select.select(rlist, wlist, xlist, self.timeout) except select.error as e: print(e) break for s in readable: # is there anything on the tunnel interface? if s is self.tunnel: # yes there is, read the packet or packets off the tunnel message = os.read(self.tunnel, 4096) if self.os_type == common.OS_MACOSX: message = message[4:] while True: # dumb check, but seems to be working. The packet has # to be longer than 4 and it must be IPv4 if (len(message) < 4) or (message[0:1] != "\x45"): #Only care about IPv4 break packetlen = struct.unpack(">H", message[2:4])[0] if packetlen == 0: break # is the rest less than the packet length? if packetlen > len(message): # in case it is less, we need to read more message += os.read(self.tunnel, 4096) readytogo = message[0:packetlen] message = message[packetlen:] # looking for client for c in self.clients: if c.get_private_ip_addr() == readytogo[16:20]: # client found, writing packet on client's pipe try: os.write(c.get_pipe_w(), readytogo) # flushing, no buffering please c.get_pipe_w_fd().flush() except: # it can break if there is a race condition # the client was found above but in the # same time the client left and the pipe # got closed. Broken pipe would be raised pass return # some ideas were taken from: https://github.com/boytm/minivtun-win/ def run_windows(self): import win32file import win32event import pywintypes import winerror import win32api # creating events, overlapped structures and a buffer for reading and writing hEvent_read = win32event.CreateEvent(None, 0, 0, None) overlapped_read = pywintypes.OVERLAPPED() overlapped_read.hEvent = hEvent_read overlapped_write = pywintypes.OVERLAPPED() message = win32file.AllocateReadBuffer(4096) while not self._stop: try: # Overlapped/async read, it either blocks or returns pending hr, _ = win32file.ReadFile(self.tunnel, message, overlapped_read) if (hr == winerror.ERROR_IO_PENDING): # when the event gets signalled or timeout happens it will return rc = win32event.WaitForSingleObject(hEvent_read, int(self.timeout*1000)) if rc == winerror.WAIT_TIMEOUT: # timed out, just rerun read continue if rc == win32event.WAIT_OBJECT_0: # read happened, packet is in "message" if (overlapped_read.InternalHigh < 4) or (message[0:1] != "\x45"): #Only care about IPv4 # too small which should not happen or not IPv4, so we just drop it. continue # reading out the packet from the buffer and discarding the rest readytogo = message[0:overlapped_read.InternalHigh] # looking for client for c in self.clients: if c.get_private_ip_addr() == readytogo[16:20]: # client found, writing packet on client's pipe # ignoring outcome, it is async so it will happen when it will happen ;) win32file.WriteFile(c.get_pipe_w(), readytogo, overlapped_write) except win32api.error as e: if e.args[0] == 995: common.internal_print("Interface disappered, exiting PS thread: {0}".format(e), -1) self.stop() continue if e.args[0] == 1453: common.internal_print("OS Internal error: {0}".format(e), -1) self.stop() continue common.internal_print("PS Exception: {0}".format(e), -1) return # stop the so called infinite loop def stop(self): self._stop = True return