├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md └── tsproxy.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | .idea/vcs.xml 64 | log.txt 65 | .idea/encodings.xml 66 | .idea/misc.xml 67 | .idea/modules.xml 68 | .idea/tsproxy.iml 69 | .idea/workspace.xml 70 | .vscode/settings.json 71 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | 3 | COPY ./tsproxy.py /usr/src/tsproxy/ 4 | WORKDIR /usr/src/tsproxy/ 5 | RUN chmod -v a+x ./tsproxy.py 6 | 7 | ENTRYPOINT [ "./tsproxy.py", "--bind", "0.0.0.0" ] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tsproxy 2 | Traffic-shaping SOCKS5 proxy 3 | 4 | tsproxy provides basic latency, download and upload traffic shaping while only requiring user-level access (no root permissions required). It should work for basic browser testing but for protocol-level work it does not provide a suitable replacement for something like dummynet or netem. 5 | 6 | tsproxy is monolithic and all of the functionality is in tsproxy.py. It is written expecting Python 2.7. 7 | 8 | # Usage 9 | ```bash 10 | $ python tsproxy.py --rtt= --inkbps= --outkbps= 11 | ``` 12 | Hit `ctrl-C` (or send a `SIGINT`) to exit 13 | 14 | # Example 15 | ```bash 16 | $ python tsproxy.py --rtt=200 --inkbps=1600 --outkbps=768 17 | ``` 18 | 19 | # Command-line Options 20 | 21 | 22 | | Option | Alias | Description | 23 | | ----------------- | -------- | ---------------------------------------- | 24 | | **`--rtt`** | **`-r`** | Latency in milliseconds (full round trip, half of the latency gets applied to each direction). | 25 | | **`--inkbps`** | **`-i`** | Download Bandwidth (in 1000 bits/s - Kbps). | 26 | | **`--outkbps`** | **`-o`** | Upload Bandwidth (in 1000 bits/s - Kbps). | 27 | | **`--window`** | **`-w`** | Emulated TCP initial congestion window (defaults to 10). | 28 | | **`--port`** | **`-p`** | SOCKS 5 proxy port (defaults to port 1080). Specifying a port of 0 will use a randomly assigned port. | 29 | | **`--bind`** | **`-b`** | Interface address to listen on (defaults to localhost). | 30 | | **`--desthost`** | **`-d`** | Redirect all outbound connections to the specified host (name or IP). | 31 | | **`--mapports`** | **`-m`** | Remap outbound ports. Comma-separated list of original:new with * as a wildcard. `--mapports '443:8443,*:8080'` | 32 | | **`--localhost`** | **`-l`** | Include connections already destined for localhost/127.0.0.1 in the host and port remapping. | 33 | | **`--nodnscache`** | **`-n`** | Disable the internal DNS cache. | 34 | | **`--flushdnscache`** | **`-f`** | Automatically flush the DNS cache 500ms after the last client disconnects. | 35 | | **`--verbose`** | **`-v`** | Increase verbosity (specify multiple times for more). `-vvvv` for full debug output. | 36 | 37 | 38 | # Runtime Options 39 | The traffic shaping configuration can be changed dynamically at runtime by passing commands in through the console (or stdin). Each command is on a line, terminated with an end-of-line (`\n`). 40 | 41 | 42 | * **`flush`** : Flush queued data out of the pipes. Useful for clearing out any accumulated background data between tests. 43 | * **`set rtt `** : Change the connection latency. i.e. `set rtt 200\n` will change to a 200ms RTT. 44 | * **`set inkbps `** : Change the download bandwidth. i.e. `set inkbps 5000\n` will change to a 5Mbps download connection. 45 | * **`set outkbps `** : Change the upload bandwidth. i.e. `set outkbps 1000\n` will change to a 1Mbps upload connection. 46 | * **`set mapports `** : Change the destination port mapping. 47 | * **`reset all`** : Disable all port mapping and traffic shaping 48 | * **`reset rtt`** : Set latency to 0 49 | * **`reset inkbps`** : Disable download traffic shaping 50 | * **`reset outkbps`** : Disable upload traffic shaping 51 | * **`reset mapports`** : Disable destination port mapping 52 | 53 | All bandwidth and latency changes also carry an implied flush and clear out any pending data. 54 | 55 | # Docker 56 | A Dockerfile is provided to allow Docker development workflow. 57 | 58 | Also, an [official image on Docker Hub](https://hub.docker.com/r/webpagetest/tsproxy/) is available from source via an [Automated Build](https://docs.docker.com/docker-hub/builds/) to enable use of tsproxy in Docker environments. You can run tsproxy without installing anything (other than Docker) by issuing: 59 | ```bash 60 | docker run --rm -it -p 1080:1080 webpagetest/tsproxy [options...] 61 | ``` 62 | 63 | # Configuring Chrome to use tsproxy 64 | Add a --proxy-server command-line option. 65 | ```bash 66 | --proxy-server="socks://localhost:1080" 67 | ``` 68 | 69 | # Known Shortcomings/Issues 70 | * DNS lookups on OSX (and FreeBSD) will block each other when it comes to actually resolving. DNS in Python on most platforms is allowed to run concurrently in threads (which tsproxy does) but on OSX and FreeBSD it is not thread-safe and there is a lock around the actual lookups. For most cases this isn't an issue because the latency isn't added on the actual DNS lookup (it is from the browser perspective but it is added outside of the actual lookup). This is also not an issue when desthost is used to override the destination address since dns lookups will be disabled. 71 | * QUIC support. Chrome [doesn't currently support QUIC proxies](https://bugs.chromium.org/p/chromium/issues/detail?id=335275), and further work would be neccessary to correctly handle UDP traffic in tsproxy. 72 | -------------------------------------------------------------------------------- /tsproxy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Copyright 2016 Google Inc. All Rights Reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | """ 17 | import asyncore 18 | import gc 19 | import logging 20 | import platform 21 | try: 22 | from Queue import Queue 23 | from Queue import Empty 24 | except ImportError: 25 | from queue import Queue 26 | from queue import Empty 27 | import re 28 | import signal 29 | import socket 30 | import sys 31 | import threading 32 | import time 33 | 34 | server = None 35 | in_pipe = None 36 | out_pipe = None 37 | must_exit = False 38 | options = None 39 | dest_addresses = None 40 | connections = {} 41 | dns_cache = {} 42 | port_mappings = None 43 | map_localhost = False 44 | needs_flush = False 45 | flush_pipes = False 46 | last_activity = None 47 | last_client_disconnected = None 48 | REMOVE_TCP_OVERHEAD = 1460.0 / 1500.0 49 | lock = threading.Lock() 50 | background_activity_count = 0 51 | current_time = time.clock if sys.platform == "win32" else time.time 52 | try: 53 | import monotonic 54 | current_time = monotonic.monotonic 55 | except Exception: 56 | pass 57 | 58 | 59 | def PrintMessage(msg): 60 | # Print the message to stdout & flush to make sure that the message is not 61 | # buffered when tsproxy is run as a subprocess. 62 | sys.stdout.write(msg + '\n') 63 | sys.stdout.flush() 64 | 65 | ######################################################################################################################## 66 | # Traffic-shaping pipe (just passthrough for now) 67 | ######################################################################################################################## 68 | class TSPipe(): 69 | PIPE_IN = 0 70 | PIPE_OUT = 1 71 | 72 | def __init__(self, direction, latency, kbps): 73 | self.direction = direction 74 | self.latency = latency 75 | self.kbps = kbps 76 | self.queue = Queue() 77 | self.last_tick = current_time() 78 | self.next_message = None 79 | self.available_bytes = .0 80 | self.peer = 'server' 81 | if self.direction == self.PIPE_IN: 82 | self.peer = 'client' 83 | 84 | def SendMessage(self, message, main_thread = True): 85 | global connections, in_pipe, out_pipe 86 | message_sent = False 87 | now = current_time() 88 | if message['message'] == 'closed': 89 | message['time'] = now 90 | else: 91 | message['time'] = current_time() + self.latency 92 | message['size'] = .0 93 | if 'data' in message: 94 | message['size'] = float(len(message['data'])) 95 | try: 96 | connection_id = message['connection'] 97 | # Send messages directly, bypassing the queues is throttling is disabled and we are on the main thread 98 | if main_thread and connection_id in connections and self.peer in connections[connection_id]and self.latency == 0 and self.kbps == .0: 99 | message_sent = self.SendPeerMessage(message) 100 | except: 101 | pass 102 | if not message_sent: 103 | try: 104 | self.queue.put(message) 105 | except: 106 | pass 107 | 108 | def SendPeerMessage(self, message): 109 | global last_activity, last_client_disconnected 110 | last_activity = current_time() 111 | message_sent = False 112 | connection_id = message['connection'] 113 | if connection_id in connections: 114 | if self.peer in connections[connection_id]: 115 | try: 116 | connections[connection_id][self.peer].handle_message(message) 117 | message_sent = True 118 | except: 119 | # Clean up any disconnected connections 120 | try: 121 | connections[connection_id]['server'].close() 122 | except: 123 | pass 124 | try: 125 | connections[connection_id]['client'].close() 126 | except: 127 | pass 128 | del connections[connection_id] 129 | if not connections: 130 | last_client_disconnected = current_time() 131 | logging.info('[{0:d}] Last connection closed'.format(self.client_id)) 132 | return message_sent 133 | 134 | def tick(self): 135 | global connections 136 | global flush_pipes 137 | next_packet_time = None 138 | processed_messages = False 139 | now = current_time() 140 | try: 141 | if self.next_message is None: 142 | self.next_message = self.queue.get_nowait() 143 | 144 | # Accumulate bandwidth if an available packet/message was waiting since our last tick 145 | if self.next_message is not None and self.kbps > .0 and self.next_message['time'] <= now: 146 | elapsed = now - self.last_tick 147 | accumulated_bytes = elapsed * self.kbps * 1000.0 / 8.0 148 | self.available_bytes += accumulated_bytes 149 | 150 | # process messages as long as the next message is sendable (latency or available bytes) 151 | while (self.next_message is not None) and\ 152 | (flush_pipes or ((self.next_message['time'] <= now) and 153 | (self.kbps <= .0 or self.next_message['size'] <= self.available_bytes))): 154 | processed_messages = True 155 | message = self.next_message 156 | self.next_message = None 157 | if self.kbps > .0: 158 | self.available_bytes -= message['size'] 159 | try: 160 | self.SendPeerMessage(message) 161 | except: 162 | pass 163 | self.next_message = self.queue.get_nowait() 164 | except Empty: 165 | pass 166 | except Exception as e: 167 | logging.exception('Tick Exception') 168 | 169 | # Only accumulate bytes while we have messages that are ready to send 170 | if self.next_message is None or self.next_message['time'] > now: 171 | self.available_bytes = .0 172 | self.last_tick = now 173 | 174 | # Figure out how long until the next packet can be sent 175 | if self.next_message is not None: 176 | # First, just the latency 177 | next_packet_time = self.next_message['time'] - now 178 | # Additional time for bandwidth 179 | if self.kbps > .0: 180 | accumulated_bytes = self.available_bytes + next_packet_time * self.kbps * 1000.0 / 8.0 181 | needed_bytes = self.next_message['size'] - accumulated_bytes 182 | if needed_bytes > 0: 183 | needed_time = needed_bytes / (self.kbps * 1000.0 / 8.0) 184 | next_packet_time += needed_time 185 | 186 | return next_packet_time 187 | 188 | 189 | ######################################################################################################################## 190 | # Threaded DNS resolver 191 | ######################################################################################################################## 192 | class AsyncDNS(threading.Thread): 193 | def __init__(self, client_id, hostname, port, is_localhost, result_pipe): 194 | threading.Thread.__init__(self) 195 | self.hostname = hostname 196 | self.port = port 197 | self.client_id = client_id 198 | self.is_localhost = is_localhost 199 | self.result_pipe = result_pipe 200 | 201 | def run(self): 202 | global lock, background_activity_count 203 | try: 204 | logging.debug('[{0:d}] AsyncDNS - calling getaddrinfo for {1}:{2:d}'.format(self.client_id, self.hostname, self.port)) 205 | addresses = socket.getaddrinfo(self.hostname, self.port) 206 | logging.info('[{0:d}] Resolving {1}:{2:d} Completed'.format(self.client_id, self.hostname, self.port)) 207 | except: 208 | addresses = () 209 | logging.info('[{0:d}] Resolving {1}:{2:d} Failed'.format(self.client_id, self.hostname, self.port)) 210 | message = {'message': 'resolved', 'connection': self.client_id, 'addresses': addresses, 'localhost': self.is_localhost} 211 | self.result_pipe.SendMessage(message, False) 212 | lock.acquire() 213 | if background_activity_count > 0: 214 | background_activity_count -= 1 215 | lock.release() 216 | # open and close a local socket which will interrupt the long polling loop to process the message 217 | s = socket.socket() 218 | s.connect((server.ipaddr, server.port)) 219 | s.close() 220 | 221 | 222 | ######################################################################################################################## 223 | # TCP Client 224 | ######################################################################################################################## 225 | class TCPConnection(asyncore.dispatcher): 226 | STATE_ERROR = -1 227 | STATE_IDLE = 0 228 | STATE_RESOLVING = 1 229 | STATE_CONNECTING = 2 230 | STATE_CONNECTED = 3 231 | 232 | def __init__(self, client_id): 233 | global options 234 | asyncore.dispatcher.__init__(self) 235 | self.client_id = client_id 236 | self.state = self.STATE_IDLE 237 | self.buffer = '' 238 | self.addr = None 239 | self.dns_thread = None 240 | self.hostname = None 241 | self.port = None 242 | self.needs_config = True 243 | self.needs_close = False 244 | self.did_resolve = False 245 | 246 | def SendMessage(self, type, message): 247 | message['message'] = type 248 | message['connection'] = self.client_id 249 | in_pipe.SendMessage(message) 250 | 251 | def handle_message(self, message): 252 | if message['message'] == 'data' and 'data' in message and len(message['data']): 253 | self.buffer += message['data'] 254 | if self.state == self.STATE_CONNECTED: 255 | self.handle_write() 256 | elif message['message'] == 'resolve': 257 | self.HandleResolve(message) 258 | elif message['message'] == 'connect': 259 | self.HandleConnect(message) 260 | elif message['message'] == 'closed': 261 | if len(self.buffer) == 0: 262 | self.handle_close() 263 | else: 264 | self.needs_close = True 265 | 266 | def handle_error(self): 267 | logging.warning('[{0:d}] Error'.format(self.client_id)) 268 | if self.state == self.STATE_CONNECTING: 269 | self.SendMessage('connected', {'success': False, 'address': self.addr}) 270 | 271 | def handle_close(self): 272 | global last_client_disconnected 273 | logging.info('[{0:d}] Server Connection Closed'.format(self.client_id)) 274 | self.state = self.STATE_ERROR 275 | self.close() 276 | try: 277 | if self.client_id in connections: 278 | if 'server' in connections[self.client_id]: 279 | del connections[self.client_id]['server'] 280 | if 'client' in connections[self.client_id]: 281 | self.SendMessage('closed', {}) 282 | else: 283 | del connections[self.client_id] 284 | if not connections: 285 | last_client_disconnected = current_time() 286 | logging.info('[{0:d}] Last Browser disconnected'.format(self.client_id)) 287 | except: 288 | pass 289 | 290 | def handle_connect(self): 291 | if self.state == self.STATE_CONNECTING: 292 | self.state = self.STATE_CONNECTED 293 | self.SendMessage('connected', {'success': True, 'address': self.addr}) 294 | logging.info('[{0:d}] Connected'.format(self.client_id)) 295 | self.handle_write() 296 | 297 | def writable(self): 298 | if self.state == self.STATE_CONNECTING: 299 | return True 300 | return len(self.buffer) > 0 301 | 302 | def handle_write(self): 303 | if self.needs_config: 304 | self.needs_config = False 305 | self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 306 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 128 * 1024) 307 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 128 * 1024) 308 | if len(self.buffer) > 0: 309 | sent = self.send(self.buffer) 310 | logging.debug('[{0:d}] TCP => {1:d} byte(s)'.format(self.client_id, sent)) 311 | self.buffer = self.buffer[sent:] 312 | if self.needs_close and len(self.buffer) == 0: 313 | self.needs_close = False 314 | self.handle_close() 315 | 316 | def handle_read(self): 317 | try: 318 | while True: 319 | data = self.recv(1460) 320 | if data: 321 | if self.state == self.STATE_CONNECTED: 322 | logging.debug('[{0:d}] TCP <= {1:d} byte(s)'.format(self.client_id, len(data))) 323 | self.SendMessage('data', {'data': data}) 324 | else: 325 | return 326 | except: 327 | pass 328 | 329 | def HandleResolve(self, message): 330 | global in_pipe, map_localhost, lock, background_activity_count 331 | self.did_resolve = True 332 | is_localhost = False 333 | if 'hostname' in message: 334 | self.hostname = message['hostname'] 335 | self.port = 0 336 | if 'port' in message: 337 | self.port = message['port'] 338 | logging.info('[{0:d}] Resolving {1}:{2:d}'.format(self.client_id, self.hostname, self.port)) 339 | if self.hostname == 'localhost': 340 | self.hostname = '127.0.0.1' 341 | if self.hostname == '127.0.0.1': 342 | logging.info('[{0:d}] Connection to localhost detected'.format(self.client_id)) 343 | is_localhost = True 344 | if (dest_addresses is not None) and (not is_localhost or map_localhost): 345 | logging.info('[{0:d}] Resolving {1}:{2:d} to mapped address {3}'.format(self.client_id, self.hostname, self.port, dest_addresses)) 346 | self.SendMessage('resolved', {'addresses': dest_addresses, 'localhost': False}) 347 | else: 348 | lock.acquire() 349 | background_activity_count += 1 350 | lock.release() 351 | self.state = self.STATE_RESOLVING 352 | self.dns_thread = AsyncDNS(self.client_id, self.hostname, self.port, is_localhost, in_pipe) 353 | self.dns_thread.start() 354 | 355 | def HandleConnect(self, message): 356 | global map_localhost 357 | if 'addresses' in message and len(message['addresses']): 358 | self.state = self.STATE_CONNECTING 359 | is_localhost = False 360 | if 'localhost' in message: 361 | is_localhost = message['localhost'] 362 | elif not self.did_resolve and message['addresses'][0] == '127.0.0.1': 363 | logging.info('[{0:d}] Connection to localhost detected'.format(self.client_id)) 364 | is_localhost = True 365 | if (dest_addresses is not None) and (not is_localhost or map_localhost): 366 | self.addr = dest_addresses[0] 367 | else: 368 | self.addr = message['addresses'][0] 369 | self.create_socket(self.addr[0], socket.SOCK_STREAM) 370 | addr = self.addr[4][0] 371 | if not is_localhost or map_localhost: 372 | port = GetDestPort(message['port']) 373 | else: 374 | port = message['port'] 375 | logging.info('[{0:d}] Connecting to {1}:{2:d}'.format(self.client_id, addr, port)) 376 | self.connect((addr, port)) 377 | 378 | 379 | ######################################################################################################################## 380 | # Socks5 Server 381 | ######################################################################################################################## 382 | class Socks5Server(asyncore.dispatcher): 383 | 384 | def __init__(self, host, port): 385 | asyncore.dispatcher.__init__(self) 386 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 387 | try: 388 | self.set_reuse_addr() 389 | self.bind((host, port)) 390 | self.listen(socket.SOMAXCONN) 391 | self.ipaddr, self.port = self.socket.getsockname() 392 | self.current_client_id = 0 393 | except: 394 | PrintMessage("Unable to listen on {0}:{1}. Is the port already in use?".format(host, port)) 395 | exit(1) 396 | 397 | def handle_accept(self): 398 | global connections, last_client_disconnected 399 | pair = self.accept() 400 | if pair is not None: 401 | last_client_disconnected = None 402 | sock, addr = pair 403 | self.current_client_id += 1 404 | logging.info('[{0:d}] Incoming connection from {1}'.format(self.current_client_id, repr(addr))) 405 | connections[self.current_client_id] = { 406 | 'client' : Socks5Connection(sock, self.current_client_id), 407 | 'server' : None 408 | } 409 | 410 | 411 | # Socks5 reference: https://en.wikipedia.org/wiki/SOCKS#SOCKS5 412 | class Socks5Connection(asyncore.dispatcher): 413 | STATE_ERROR = -1 414 | STATE_WAITING_FOR_HANDSHAKE = 0 415 | STATE_WAITING_FOR_CONNECT_REQUEST = 1 416 | STATE_RESOLVING = 2 417 | STATE_CONNECTING = 3 418 | STATE_CONNECTED = 4 419 | 420 | def __init__(self, connected_socket, client_id): 421 | global options 422 | asyncore.dispatcher.__init__(self, connected_socket) 423 | self.client_id = client_id 424 | self.state = self.STATE_WAITING_FOR_HANDSHAKE 425 | self.ip = None 426 | self.addresses = None 427 | self.hostname = None 428 | self.port = None 429 | self.requested_address = None 430 | self.buffer = '' 431 | self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 432 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 128 * 1024) 433 | self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 128 * 1024) 434 | self.needs_close = False 435 | 436 | def SendMessage(self, type, message): 437 | message['message'] = type 438 | message['connection'] = self.client_id 439 | out_pipe.SendMessage(message) 440 | 441 | def handle_message(self, message): 442 | if message['message'] == 'data' and 'data' in message and len(message['data']) > 0: 443 | self.buffer += message['data'] 444 | if self.state == self.STATE_CONNECTED: 445 | self.handle_write() 446 | elif message['message'] == 'resolved': 447 | self.HandleResolved(message) 448 | elif message['message'] == 'connected': 449 | self.HandleConnected(message) 450 | self.handle_write() 451 | elif message['message'] == 'closed': 452 | if len(self.buffer) == 0: 453 | logging.info('[{0:d}] Server connection close being processed, closing Browser connection'.format(self.client_id)) 454 | self.handle_close() 455 | else: 456 | logging.info('[{0:d}] Server connection close being processed, queuing browser connection close'.format(self.client_id)) 457 | self.needs_close = True 458 | 459 | def writable(self): 460 | return len(self.buffer) > 0 461 | 462 | def handle_write(self): 463 | if len(self.buffer) > 0: 464 | sent = self.send(self.buffer) 465 | logging.debug('[{0:d}] SOCKS <= {1:d} byte(s)'.format(self.client_id, sent)) 466 | self.buffer = self.buffer[sent:] 467 | if self.needs_close and len(self.buffer) == 0: 468 | logging.info('[{0:d}] queued browser connection close being processed, closing Browser connection'.format(self.client_id)) 469 | self.needs_close = False 470 | self.handle_close() 471 | 472 | def handle_read(self): 473 | global connections 474 | global dns_cache 475 | try: 476 | while True: 477 | # Consume in up-to packet-sized chunks (TCP packet payload as 1460 bytes from 1500 byte ethernet frames) 478 | data = self.recv(1460) 479 | if data: 480 | data_len = len(data) 481 | if self.state == self.STATE_CONNECTED: 482 | logging.debug('[{0:d}] SOCKS => {1:d} byte(s)'.format(self.client_id, data_len)) 483 | self.SendMessage('data', {'data': data}) 484 | elif self.state == self.STATE_WAITING_FOR_HANDSHAKE: 485 | self.state = self.STATE_ERROR #default to an error state, set correctly if things work out 486 | if data_len >= 2 and ord(data[0]) == 0x05: 487 | supports_no_auth = False 488 | auth_count = ord(data[1]) 489 | if data_len == auth_count + 2: 490 | for i in range(auth_count): 491 | offset = i + 2 492 | if ord(data[offset]) == 0: 493 | supports_no_auth = True 494 | if supports_no_auth: 495 | # Respond with a message that "No Authentication" was agreed to 496 | logging.info('[{0:d}] New Socks5 client'.format(self.client_id)) 497 | response = chr(0x05) + chr(0x00) 498 | self.state = self.STATE_WAITING_FOR_CONNECT_REQUEST 499 | self.buffer += response 500 | self.handle_write() 501 | elif self.state == self.STATE_WAITING_FOR_CONNECT_REQUEST: 502 | self.state = self.STATE_ERROR #default to an error state, set correctly if things work out 503 | if data_len >= 10 and ord(data[0]) == 0x05 and ord(data[2]) == 0x00: 504 | if ord(data[1]) == 0x01: #TCP connection (only supported method for now) 505 | connections[self.client_id]['server'] = TCPConnection(self.client_id) 506 | self.requested_address = data[3:] 507 | port_offset = 0 508 | if ord(data[3]) == 0x01: 509 | port_offset = 8 510 | self.ip = '{0:d}.{1:d}.{2:d}.{3:d}'.format(ord(data[4]), ord(data[5]), ord(data[6]), ord(data[7])) 511 | elif ord(data[3]) == 0x03: 512 | name_len = ord(data[4]) 513 | if data_len >= 6 + name_len: 514 | port_offset = 5 + name_len 515 | self.hostname = data[5:5 + name_len] 516 | elif ord(data[3]) == 0x04 and data_len >= 22: 517 | port_offset = 20 518 | self.ip = '' 519 | for i in range(16): 520 | self.ip += '{0:02x}'.format(ord(data[4 + i])) 521 | if i % 2 and i < 15: 522 | self.ip += ':' 523 | if port_offset and connections[self.client_id]['server'] is not None: 524 | self.port = 256 * ord(data[port_offset]) + ord(data[port_offset + 1]) 525 | if self.port: 526 | if self.ip is None and self.hostname is not None: 527 | if dns_cache is not None and self.hostname in dns_cache: 528 | self.state = self.STATE_CONNECTING 529 | cache_entry = dns_cache[self.hostname] 530 | self.addresses = cache_entry['addresses'] 531 | self.SendMessage('connect', {'addresses': self.addresses, 'port': self.port, 'localhost': cache_entry['localhost']}) 532 | else: 533 | self.state = self.STATE_RESOLVING 534 | self.SendMessage('resolve', {'hostname': self.hostname, 'port': self.port}) 535 | elif self.ip is not None: 536 | self.state = self.STATE_CONNECTING 537 | logging.debug('[{0:d}] Socks Connect - calling getaddrinfo for {1}:{2:d}'.format(self.client_id, self.ip, self.port)) 538 | self.addresses = socket.getaddrinfo(self.ip, self.port) 539 | self.SendMessage('connect', {'addresses': self.addresses, 'port': self.port}) 540 | else: 541 | return 542 | except: 543 | pass 544 | 545 | def handle_close(self): 546 | global last_client_disconnected 547 | logging.info('[{0:d}] Browser Connection Closed by browser'.format(self.client_id)) 548 | self.state = self.STATE_ERROR 549 | self.close() 550 | try: 551 | if self.client_id in connections: 552 | if 'client' in connections[self.client_id]: 553 | del connections[self.client_id]['client'] 554 | if 'server' in connections[self.client_id]: 555 | self.SendMessage('closed', {}) 556 | else: 557 | del connections[self.client_id] 558 | if not connections: 559 | last_client_disconnected = current_time() 560 | logging.info('[{0:d}] Last Browser disconnected'.format(self.client_id)) 561 | except: 562 | pass 563 | 564 | def HandleResolved(self, message): 565 | global dns_cache 566 | if self.state == self.STATE_RESOLVING: 567 | if 'addresses' in message and len(message['addresses']): 568 | self.state = self.STATE_CONNECTING 569 | self.addresses = message['addresses'] 570 | if dns_cache is not None: 571 | dns_cache[self.hostname] = {'addresses': self.addresses, 'localhost': message['localhost']} 572 | logging.debug('[{0:d}] Resolved {1}, Connecting'.format(self.client_id, self.hostname)) 573 | self.SendMessage('connect', {'addresses': self.addresses, 'port': self.port, 'localhost': message['localhost']}) 574 | else: 575 | # Send host unreachable error 576 | self.state = self.STATE_ERROR 577 | self.buffer += chr(0x05) + chr(0x04) + self.requested_address 578 | self.handle_write() 579 | 580 | def HandleConnected(self, message): 581 | if 'success' in message and self.state == self.STATE_CONNECTING: 582 | response = chr(0x05) 583 | if message['success']: 584 | response += chr(0x00) 585 | logging.debug('[{0:d}] Connected to {1}'.format(self.client_id, self.hostname)) 586 | self.state = self.STATE_CONNECTED 587 | else: 588 | response += chr(0x04) 589 | self.state = self.STATE_ERROR 590 | response += chr(0x00) 591 | response += self.requested_address 592 | self.buffer += response 593 | self.handle_write() 594 | 595 | 596 | ######################################################################################################################## 597 | # stdin command processor 598 | ######################################################################################################################## 599 | class CommandProcessor(): 600 | def __init__(self): 601 | thread = threading.Thread(target = self.run, args=()) 602 | thread.daemon = True 603 | thread.start() 604 | 605 | def run(self): 606 | global must_exit 607 | while not must_exit: 608 | for line in iter(sys.stdin.readline, ''): 609 | self.ProcessCommand(line.strip()) 610 | 611 | def ProcessCommand(self, input): 612 | global in_pipe 613 | global out_pipe 614 | global needs_flush 615 | global REMOVE_TCP_OVERHEAD 616 | global port_mappings 617 | global server 618 | global must_exit 619 | if len(input): 620 | ok = False 621 | try: 622 | command = input.split() 623 | if len(command) and len(command[0]): 624 | if command[0].lower() == 'flush': 625 | ok = True 626 | elif command[0].lower() == 'set' and len(command) >= 3: 627 | if command[1].lower() == 'rtt' and len(command[2]): 628 | rtt = float(command[2]) 629 | latency = rtt / 2000.0 630 | in_pipe.latency = latency 631 | out_pipe.latency = latency 632 | ok = True 633 | elif command[1].lower() == 'inkbps' and len(command[2]): 634 | in_pipe.kbps = float(command[2]) * REMOVE_TCP_OVERHEAD 635 | ok = True 636 | elif command[1].lower() == 'outkbps' and len(command[2]): 637 | out_pipe.kbps = float(command[2]) * REMOVE_TCP_OVERHEAD 638 | ok = True 639 | elif command[1].lower() == 'mapports' and len(command[2]): 640 | SetPortMappings(command[2]) 641 | ok = True 642 | elif command[0].lower() == 'reset' and len(command) >= 2: 643 | if command[1].lower() == 'rtt' or command[1].lower() == 'all': 644 | in_pipe.latency = 0 645 | out_pipe.latency = 0 646 | ok = True 647 | if command[1].lower() == 'inkbps' or command[1].lower() == 'all': 648 | in_pipe.kbps = 0 649 | ok = True 650 | if command[1].lower() == 'outkbps' or command[1].lower() == 'all': 651 | out_pipe.kbps = 0 652 | ok = True 653 | if command[1].lower() == 'mapports' or command[1].lower() == 'all': 654 | port_mappings = {} 655 | ok = True 656 | elif command[0].lower() == 'exit': 657 | must_exit = True 658 | ok = True 659 | 660 | if ok: 661 | needs_flush = True 662 | except: 663 | pass 664 | if not ok: 665 | PrintMessage('ERROR') 666 | # open and close a local socket which will interrupt the long polling loop to process the flush 667 | if needs_flush: 668 | s = socket.socket() 669 | s.connect((server.ipaddr, server.port)) 670 | s.close() 671 | 672 | 673 | ######################################################################################################################## 674 | # Main Entry Point 675 | ######################################################################################################################## 676 | def main(): 677 | global server 678 | global options 679 | global in_pipe 680 | global out_pipe 681 | global dest_addresses 682 | global port_mappings 683 | global map_localhost 684 | global dns_cache 685 | import argparse 686 | global REMOVE_TCP_OVERHEAD 687 | parser = argparse.ArgumentParser(description='Traffic-shaping socks5 proxy.', 688 | prog='tsproxy') 689 | parser.add_argument('-v', '--verbose', action='count', help="Increase verbosity (specify multiple times for more). -vvvv for full debug output.") 690 | parser.add_argument('--logfile', help="Write log messages to given file instead of stdout.") 691 | parser.add_argument('-b', '--bind', default='localhost', help="Server interface address (defaults to localhost).") 692 | parser.add_argument('-p', '--port', type=int, default=1080, help="Server port (defaults to 1080, use 0 for randomly assigned).") 693 | parser.add_argument('-r', '--rtt', type=float, default=.0, help="Round Trip Time Latency (in ms).") 694 | parser.add_argument('-i', '--inkbps', type=float, default=.0, help="Download Bandwidth (in 1000 bits/s - Kbps).") 695 | parser.add_argument('-o', '--outkbps', type=float, default=.0, help="Upload Bandwidth (in 1000 bits/s - Kbps).") 696 | parser.add_argument('-w', '--window', type=int, default=10, help="Emulated TCP initial congestion window (defaults to 10).") 697 | parser.add_argument('-d', '--desthost', help="Redirect all outbound connections to the specified host.") 698 | parser.add_argument('-m', '--mapports', help="Remap outbound ports. Comma-separated list of original:new with * as a wildcard. --mapports '443:8443,*:8080'") 699 | parser.add_argument('-l', '--localhost', action='store_true', default=False, 700 | help="Include connections already destined for localhost/127.0.0.1 in the host and port remapping.") 701 | parser.add_argument('-n', '--nodnscache', action='store_true', default=False, help="Disable internal DNS cache.") 702 | parser.add_argument('-f', '--flushdnscache', action='store_true', default=False, help="Automatically flush the DNS cache 500ms after the last client disconnects.") 703 | options = parser.parse_args() 704 | 705 | # Set up logging 706 | log_level = logging.CRITICAL 707 | if options.verbose == 1: 708 | log_level = logging.ERROR 709 | elif options.verbose == 2: 710 | log_level = logging.WARNING 711 | elif options.verbose == 3: 712 | log_level = logging.INFO 713 | elif options.verbose >= 4: 714 | log_level = logging.DEBUG 715 | if options.logfile is not None: 716 | logging.basicConfig(filename=options.logfile, level=log_level, 717 | format="%(asctime)s.%(msecs)03d - %(message)s", datefmt="%H:%M:%S") 718 | else: 719 | logging.basicConfig(level=log_level, format="%(asctime)s.%(msecs)03d - %(message)s", datefmt="%H:%M:%S") 720 | 721 | # Parse any port mappings 722 | if options.mapports: 723 | SetPortMappings(options.mapports) 724 | 725 | if options.nodnscache: 726 | dns_cache = None 727 | 728 | map_localhost = options.localhost 729 | 730 | # Resolve the address for a rewrite destination host if one was specified 731 | if options.desthost: 732 | logging.debug('Startup - calling getaddrinfo for {0}:{1:d}'.format(options.desthost, GetDestPort(80))) 733 | dest_addresses = socket.getaddrinfo(options.desthost, GetDestPort(80)) 734 | 735 | # Set up the pipes. 1/2 of the latency gets applied in each direction (and /1000 to convert to seconds) 736 | in_pipe = TSPipe(TSPipe.PIPE_IN, options.rtt / 2000.0, options.inkbps * REMOVE_TCP_OVERHEAD) 737 | out_pipe = TSPipe(TSPipe.PIPE_OUT, options.rtt / 2000.0, options.outkbps * REMOVE_TCP_OVERHEAD) 738 | 739 | signal.signal(signal.SIGINT, signal_handler) 740 | server = Socks5Server(options.bind, options.port) 741 | command_processor = CommandProcessor() 742 | PrintMessage('Started Socks5 proxy server on {0}:{1:d}\nHit Ctrl-C to exit.'.format(server.ipaddr, server.port)) 743 | run_loop() 744 | 745 | def signal_handler(signal, frame): 746 | global server 747 | global must_exit 748 | logging.error('Exiting...') 749 | must_exit = True 750 | del server 751 | 752 | 753 | # Wrapper around the asyncore loop that lets us poll the in/out pipes every 1ms 754 | def run_loop(): 755 | global must_exit 756 | global in_pipe 757 | global out_pipe 758 | global needs_flush 759 | global flush_pipes 760 | global last_activity 761 | global last_client_disconnected 762 | global dns_cache 763 | winmm = None 764 | 765 | # increase the windows timer resolution to 1ms 766 | if platform.system() == "Windows": 767 | try: 768 | import ctypes 769 | winmm = ctypes.WinDLL('winmm') 770 | winmm.timeBeginPeriod(1) 771 | except: 772 | pass 773 | 774 | last_activity = current_time() 775 | last_check = current_time() 776 | # disable gc to avoid pauses during traffic shaping/proxying 777 | gc.disable() 778 | out_interval = None 779 | in_interval = None 780 | while not must_exit: 781 | # Tick every 1ms if traffic-shaping is enabled and we have data or are doing background dns lookups, every 1 second otherwise 782 | lock.acquire() 783 | tick_interval = 0.001 784 | if out_interval is not None: 785 | tick_interval = max(tick_interval, out_interval) 786 | if in_interval is not None: 787 | tick_interval = max(tick_interval, in_interval) 788 | if background_activity_count == 0: 789 | if in_pipe.next_message is None and in_pipe.queue.empty() and out_pipe.next_message is None and out_pipe.queue.empty(): 790 | tick_interval = 1.0 791 | elif in_pipe.kbps == .0 and in_pipe.latency == 0 and out_pipe.kbps == .0 and out_pipe.latency == 0: 792 | tick_interval = 1.0 793 | lock.release() 794 | logging.debug("Tick Time: %0.3f", tick_interval) 795 | asyncore.poll(tick_interval, asyncore.socket_map) 796 | if needs_flush: 797 | flush_pipes = True 798 | dns_cache = {} 799 | needs_flush = False 800 | out_interval = out_pipe.tick() 801 | in_interval = in_pipe.tick() 802 | if flush_pipes: 803 | PrintMessage('OK') 804 | flush_pipes = False 805 | now = current_time() 806 | # Clear the DNS cache 500ms after the last client disconnects 807 | if options.flushdnscache and last_client_disconnected is not None and dns_cache: 808 | if now - last_client_disconnected >= 0.5: 809 | dns_cache = {} 810 | last_client_disconnected = None 811 | logging.debug("Flushed DNS cache") 812 | # Every 500 ms check to see if it is a good time to do a gc 813 | if now - last_check >= 0.5: 814 | last_check = now 815 | # manually gc after 5 seconds of idle 816 | if now - last_activity >= 5: 817 | last_activity = now 818 | logging.debug("Triggering manual GC") 819 | gc.collect() 820 | 821 | if winmm is not None: 822 | winmm.timeEndPeriod(1) 823 | 824 | def GetDestPort(port): 825 | global port_mappings 826 | if port_mappings is not None: 827 | src_port = str(port) 828 | if src_port in port_mappings: 829 | return port_mappings[src_port] 830 | elif 'default' in port_mappings: 831 | return port_mappings['default'] 832 | return port 833 | 834 | 835 | def SetPortMappings(map_string): 836 | global port_mappings 837 | port_mappings = {} 838 | map_string = map_string.strip('\'" \t\r\n') 839 | for pair in map_string.split(','): 840 | (src, dest) = pair.split(':') 841 | if src == '*': 842 | port_mappings['default'] = int(dest) 843 | logging.debug("Default port mapped to port {0}".format(dest)) 844 | else: 845 | logging.debug("Port {0} mapped to port {1}".format(src, dest)) 846 | port_mappings[src] = int(dest) 847 | 848 | 849 | if '__main__' == __name__: 850 | main() 851 | --------------------------------------------------------------------------------