├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── build_singlefile_slaver.py ├── common_func.py ├── master.py └── slaver.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### macOS template 3 | *.DS_Store 4 | .AppleDouble 5 | .LSOverride 6 | 7 | # Icon must end with two \r 8 | Icon 9 | 10 | 11 | # Thumbnails 12 | ._* 13 | 14 | # Files that might appear in the root of a volume 15 | .DocumentRevisions-V100 16 | .fseventsd 17 | .Spotlight-V100 18 | .TemporaryItems 19 | .Trashes 20 | .VolumeIcon.icns 21 | .com.apple.timemachine.donotpresent 22 | 23 | # Directories potentially created on remote AFP share 24 | .AppleDB 25 | .AppleDesktop 26 | Network Trash Folder 27 | Temporary Items 28 | .apdisk 29 | ### Archives template 30 | # It's better to unpack these files and commit the raw source because 31 | # git has its own built in compression methods. 32 | *.7z 33 | *.jar 34 | *.rar 35 | *.zip 36 | *.gz 37 | *.bzip 38 | *.bz2 39 | *.xz 40 | *.lzma 41 | *.cab 42 | 43 | #packing-only formats 44 | *.iso 45 | *.tar 46 | 47 | #package management formats 48 | *.dmg 49 | *.xpi 50 | *.gem 51 | *.egg 52 | *.deb 53 | *.rpm 54 | *.msi 55 | *.msm 56 | *.msp 57 | ### Ansible template 58 | *.retry 59 | ### MicrosoftOffice template 60 | *.tmp 61 | 62 | # Word temporary 63 | ~$*.doc* 64 | 65 | # Excel temporary 66 | ~$*.xls* 67 | 68 | # Excel Backup File 69 | *.xlk 70 | 71 | # PowerPoint temporary 72 | ~$*.ppt* 73 | 74 | # Visio autosave temporary files 75 | *.~vsdx 76 | ### Windows template 77 | # Windows image file caches 78 | Thumbs.db 79 | ehthumbs.db 80 | 81 | # Folder config file 82 | Desktop.ini 83 | 84 | # Recycle Bin used on file shares 85 | $RECYCLE.BIN/ 86 | 87 | # Windows Installer files 88 | 89 | # Windows shortcuts 90 | *.lnk 91 | ### NotepadPP template 92 | # Notepad++ backups # 93 | *.bak 94 | ### Linux template 95 | *~ 96 | 97 | # temporary files which can be created if a process still has a handle open of a deleted file 98 | .fuse_hidden* 99 | 100 | # KDE directory preferences 101 | .directory 102 | 103 | # Linux trash folder which might appear on any partition or disk 104 | .Trash-* 105 | 106 | # .nfs files are created when an open file is removed but is still being accessed 107 | .nfs* 108 | ### Python template 109 | # Byte-compiled / optimized / DLL files 110 | __pycache__/ 111 | *.py[cod] 112 | *$py.class 113 | 114 | # C extensions 115 | *.so 116 | 117 | # Distribution / packaging 118 | .Python 119 | env/ 120 | build/ 121 | develop-eggs/ 122 | dist/ 123 | downloads/ 124 | eggs/ 125 | .eggs/ 126 | lib/ 127 | lib64/ 128 | parts/ 129 | sdist/ 130 | var/ 131 | *.egg-info/ 132 | .installed.cfg 133 | 134 | # PyInstaller 135 | # Usually these files are written by a python script from a template 136 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 137 | *.manifest 138 | *.spec 139 | 140 | # Installer logs 141 | pip-log.txt 142 | pip-delete-this-directory.txt 143 | 144 | # Unit test / coverage reports 145 | htmlcov/ 146 | .tox/ 147 | .coverage 148 | .coverage.* 149 | .cache 150 | nosetests.xml 151 | coverage.xml 152 | *,cover 153 | .hypothesis/ 154 | 155 | # Translations 156 | *.mo 157 | *.pot 158 | 159 | # Django stuff: 160 | *.log 161 | local_settings.py 162 | 163 | # Flask stuff: 164 | instance/ 165 | .webassets-cache 166 | 167 | # Scrapy stuff: 168 | .scrapy 169 | 170 | # Sphinx documentation 171 | docs/_build/ 172 | 173 | # PyBuilder 174 | target/ 175 | 176 | # Jupyter Notebook 177 | .ipynb_checkpoints 178 | 179 | # pyenv 180 | .python-version 181 | 182 | # celery beat schedule file 183 | celerybeat-schedule 184 | 185 | # dotenv 186 | .env 187 | 188 | # virtualenv 189 | .venv/ 190 | venv/ 191 | ENV/ 192 | 193 | # Spyder project settings 194 | .spyderproject 195 | 196 | # Rope project settings 197 | .ropeproject 198 | ### Vim template 199 | # swap 200 | [._]*.s[a-w][a-z] 201 | [._]s[a-w][a-z] 202 | # session 203 | Session.vim 204 | # temporary 205 | .netrwhist 206 | # auto-generated tag files 207 | tags 208 | ### VisualStudioCode template 209 | .vscode/* 210 | !.vscode/settings.json 211 | !.vscode/tasks.json 212 | !.vscode/launch.json 213 | !.vscode/extensions.json 214 | ### JetBrains template 215 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 216 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 217 | /.idea 218 | 219 | ## File-based project format: 220 | *.iws 221 | 222 | ## Plugin-specific files: 223 | 224 | # IntelliJ 225 | /out/ 226 | 227 | # mpeltonen/sbt-idea plugin 228 | .idea_modules/ 229 | 230 | # JIRA plugin 231 | atlassian-ide-plugin.xml 232 | 233 | # Crashlytics plugin (for Android Studio and IntelliJ) 234 | com_crashlytics_export_strings.xml 235 | crashlytics.properties 236 | crashlytics-build.properties 237 | fabric.properties 238 | /slaver_singlefile.py 239 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Aploium 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shootback 2 | 3 | shootback is a reverse TCP tunnel let you access target behind NAT or firewall 4 | 反向TCP隧道, 使得NAT或防火墙后的内网机器可以被外网访问. 5 | Consumes less than 1% CPU and 8MB memory under 800 concurrency. 6 | slaver is __single file__ and only depends on python(2.7/3.4+) standard library. 7 | 8 | ## How it works 9 | 10 | ![How shoot back works](https://raw.githubusercontent.com/aploium/shootback/static/graph.png) 11 | 12 | 13 | ## Typical Scene 14 | 15 | 1. Access company/school computer(no internet IP) from home 16 | 从家里连接公司或学校里没有独立外网IP的电脑 17 | 2. Make private network/site public. 18 | 使内网或内网站点能从公网访问 19 | 3. Help private network penetration. 20 | 辅助内网渗透 21 | 4. Help CTF offline competitions. 22 | 辅助CTF线下赛, 使场外选手也获得比赛网络环境 23 | 5. Connect to device with dynamic IP, such as ADSL 24 | 连接动态IP的设备, 如ADSL 25 | 6. SSL encryption between slaver and master 26 | slaver和master间支持SSL加密 27 | 28 | ## Getting started 29 | 30 | 1. requirement: 31 | * Master: Python3.4+, OS independent 32 | * Slaver: Python2.7/3.4+, OS independent 33 | * no external dependencies, only python std lib 34 | 2. download `git clone https://github.com/aploium/shootback` 35 | 3. (optional) if you need a single-file slaver.py, run `python3 build_singlefile_slaver.py` 36 | 4. run these command 37 | ```bash 38 | # master listen :10000 for slaver, :10080 for you 39 | python3 master.py -m 0.0.0.0:10000 -c 127.0.0.1:10080 40 | 41 | # slaver connect to master, and use example.com as tunnel target 42 | # ps: you can use python2 in slaver, not only py3 43 | python3 slaver.py -m 127.0.0.1:10000 -t example.com:80 44 | 45 | # doing request to master 46 | curl -v -H "host: example.com" 127.0.0.1:10080 47 | # -- some HTML content from example.com -- 48 | # -- some HTML content from example.com -- 49 | # -- some HTML content from example.com -- 50 | ``` 51 | 5. a more reality example (with ssl): 52 | assume your master is 22.33.44.55 (just like the graph above) 53 | ```bash 54 | # slaver_local_ssh <---> slaver <--[SSL]--> master(22.33.44.55) <--> You 55 | 56 | # ---- master ---- 57 | python3 master.py -m 0.0.0.0:10000 -c 0.0.0.0:10022 --ssl 58 | 59 | # ---- slaver ---- 60 | # ps: the `--ssl` option is for slaver-master encryption, not for SSH 61 | python(or python3) slaver.py -m 22.33.44.55:10000 -t 127.0.0.1:22 --ssl 62 | 63 | # ---- YOU ---- 64 | ssh 22.33.44.55 -p 10022 65 | ``` 66 | 67 | 6. for more help, please see `python3 master.py --help` and `python3 slaver.py --help` 68 | 69 | ## Tips 70 | 71 | 1. run in daemon: 72 | `nohup python(or python3) slaver.py -m host:port -t host:port -q &` 73 | or: 74 | ```bash 75 | # screen is a linux command 76 | screen 77 | python(or python3) slaver.py -m host:port -t host:port 78 | # press ctrl-a d to detach screen 79 | # and if necessary, use "screen -r" to reattach 80 | ``` 81 | 82 | 2. ANY service using TCP is shootback-able. HTTP/FTP/Proxy/SSH/VNC/... 83 | 84 | 3. shootback itself just do the transmission job, do not handle encrypt or proxy. 85 | however you can use a 3rd party proxy (eg: shadowsocks) as slaver target. 86 | for example: 87 | `shadowsocks_server<-->shootback_slaver<-->shootback_master<-->shadowsocks_client(socks5)` 88 | 89 | ## Warning 90 | 91 | 1. in windows, due to the limit of CPython `select.select()`, 92 | shootback can NOT handle more than 512 concurrency, you may meet 93 | `ValueError: too many file descriptors in select()` 94 | If you have to handle such high concurrency in windows, 95 | [Anaconda-Python3](https://www.continuum.io/downloads) is recommend, 96 | [it's limit in windows is 2048](https://github.com/ContinuumIO/anaconda-issues/issues/1241) 97 | 98 | 99 | ## Performance 100 | 101 | 1. in my laptop of intel I7-4710MQ, win10 x64: 102 | * 1.6Gbits/s of loopback transfer (using iperf), with about 5% CPU occupation. 103 | * 800 thread ApacheBench, with less than 1% CPU and 8MB memory consume 104 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /build_singlefile_slaver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | 4 | def build_singlefile_slaver(outfile_name="slaver_singlefile.py"): 5 | import os 6 | import shutil 7 | base_path = os.path.dirname(os.path.abspath(__file__)) 8 | output_fullpath = os.path.join(base_path, outfile_name) 9 | 10 | if os.path.exists(output_fullpath): 11 | ch = input("target file: " + output_fullpath + " already exist, overwrite it?\n(y/N)") 12 | if ch not in ("y", "Y", "yes"): 13 | print("user abort") 14 | return None 15 | else: 16 | os.remove(output_fullpath) 17 | 18 | shutil.copy( 19 | os.path.join(base_path, "common_func.py"), 20 | os.path.join(base_path, outfile_name, ) 21 | ) 22 | with open("slaver.py", "r", encoding='utf-8') as fr: 23 | slaver = fr.read() 24 | 25 | slaver = slaver.replace("from __future__ import", "# from __future__ import", 1) 26 | slaver = slaver.replace("from common_func import", "# from common_func import", 1) 27 | 28 | with open(outfile_name, "a", encoding="utf-8") as fw: 29 | fw.write(slaver) 30 | 31 | print("generate complete!\noutput file:", output_fullpath) 32 | 33 | 34 | if __name__ == '__main__': 35 | build_singlefile_slaver() 36 | -------------------------------------------------------------------------------- /common_func.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | from __future__ import print_function, unicode_literals, division, absolute_import 4 | import sys 5 | import time 6 | import binascii 7 | import struct 8 | import collections 9 | import logging 10 | import socket 11 | import functools 12 | import threading 13 | import traceback 14 | import warnings 15 | 16 | try: 17 | import selectors 18 | from selectors import EVENT_READ, EVENT_WRITE 19 | 20 | EVENT_READ_WRITE = EVENT_READ | EVENT_WRITE 21 | except ImportError: 22 | import select 23 | 24 | warnings.warn('selectors module not available, fallback to select') 25 | selectors = None 26 | 27 | try: 28 | import ssl 29 | except ImportError: 30 | ssl = None 31 | warnings.warn('ssl module not available, ssl feature is disabled') 32 | 33 | try: 34 | # for pycharm type hinting 35 | from typing import Union, Callable 36 | except: 37 | pass 38 | 39 | # socket recv buffer, 16384 bytes 40 | RECV_BUFFER_SIZE = 2 ** 14 41 | 42 | # default secretkey, use -k/--secretkey to change 43 | SECRET_KEY = None # "shootback" 44 | 45 | # how long a SPARE slaver would keep 46 | # once slaver received an heart-beat package from master, 47 | # the TTL would be reset. And heart-beat delay is less than TTL, 48 | # so, theoretically, spare slaver never timeout, 49 | # except network failure 50 | # notice: working slaver would NEVER timeout 51 | SPARE_SLAVER_TTL = 300 52 | 53 | # internal program version, appears in CtrlPkg 54 | INTERNAL_VERSION = 0x0013 55 | 56 | # # how many packet are buffed, before delaying recv 57 | # SOCKET_BRIDGE_SEND_BUFF_SIZE = 5 58 | 59 | # version for human readable 60 | __version__ = (2, 6, 1, INTERNAL_VERSION) 61 | 62 | # just a logger 63 | log = logging.getLogger(__name__) 64 | 65 | 66 | def version_info(): 67 | """get program version for human. eg: "2.1.0-r2" """ 68 | return "{}.{}.{}-r{}".format(*__version__) 69 | 70 | 71 | def configure_logging(level): 72 | logging.basicConfig( 73 | level=level, 74 | format='[%(levelname)s %(asctime)s] %(message)s', 75 | ) 76 | 77 | 78 | def fmt_addr(socket): 79 | """(host, int(port)) --> "host:port" """ 80 | return "{}:{}".format(*socket) 81 | 82 | 83 | def split_host(x): 84 | """ "host:port" --> (host, int(port))""" 85 | try: 86 | host, port = x.split(":") 87 | port = int(port) 88 | except: 89 | raise ValueError( 90 | "wrong syntax, format host:port is " 91 | "required, not {}".format(x)) 92 | else: 93 | return host, port 94 | 95 | 96 | def try_close(closable): 97 | """try close something 98 | 99 | same as 100 | try: 101 | connection.close() 102 | except: 103 | pass 104 | """ 105 | try: 106 | closable.close() 107 | except: 108 | pass 109 | 110 | 111 | def select_recv(conn, buff_size, timeout=None): 112 | """add timeout for socket.recv() 113 | :type conn: socket.socket 114 | :type buff_size: int 115 | :type timeout: float 116 | :rtype: Union[bytes, None] 117 | """ 118 | if selectors: 119 | sel = selectors.DefaultSelector() 120 | sel.register(conn, EVENT_READ) 121 | events = sel.select(timeout) 122 | sel.close() 123 | if not events: 124 | # timeout 125 | raise RuntimeError("recv timeout") 126 | else: 127 | rlist, _, _ = select.select([conn], [], [], timeout) 128 | 129 | buff = conn.recv(buff_size) 130 | if not buff: 131 | raise RuntimeError("received zero bytes, socket was closed") 132 | 133 | return buff 134 | 135 | 136 | def set_secretkey(key): 137 | global SECRET_KEY 138 | SECRET_KEY = key 139 | CtrlPkg.recalc_crc32() 140 | 141 | 142 | class SocketBridge(object): 143 | """ 144 | transfer data between sockets 145 | """ 146 | 147 | def __init__(self): 148 | self.conn_rd = set() # record readable-sockets 149 | self.conn_wr = set() # record writeable-sockets 150 | self.map = {} # record sockets pairs 151 | self.callbacks = {} # record callbacks 152 | self.send_buff = {} # buff one packet for those sending too-fast socket 153 | 154 | if selectors: 155 | self.sel = selectors.DefaultSelector() 156 | else: 157 | self.sel = None 158 | 159 | def add_conn_pair(self, conn1, conn2, callback=None): 160 | """ 161 | transfer anything between two sockets 162 | 163 | :type conn1: socket.socket 164 | :type conn2: socket.socket 165 | :param callback: callback in connection finish 166 | :type callback: Callable 167 | """ 168 | # change to non-blocking 169 | # we use select or epoll to notice when data is ready 170 | conn1.setblocking(False) 171 | conn2.setblocking(False) 172 | 173 | # mark as readable+writable 174 | self.conn_rd.add(conn1) 175 | self.conn_wr.add(conn1) 176 | self.conn_rd.add(conn2) 177 | self.conn_wr.add(conn2) 178 | 179 | # record sockets pairs 180 | self.map[conn1] = conn2 181 | self.map[conn2] = conn1 182 | 183 | # record callback 184 | if callback is not None: 185 | self.callbacks[conn1] = callback 186 | 187 | if self.sel: 188 | self.sel.register(conn1, EVENT_READ_WRITE) 189 | self.sel.register(conn2, EVENT_READ_WRITE) 190 | 191 | def start_as_daemon(self): 192 | t = threading.Thread(target=self.start) 193 | t.daemon = True 194 | t.start() 195 | log.info("SocketBridge daemon started") 196 | return t 197 | 198 | def start(self): 199 | while True: 200 | try: 201 | self._start() 202 | except: 203 | log.error("FATAL ERROR! SocketBridge failed {}".format( 204 | traceback.format_exc() 205 | )) 206 | 207 | def _start(self): 208 | 209 | while True: 210 | if not self.conn_rd and not self.conn_wr: 211 | # sleep if there is no connections 212 | time.sleep(0.01) 213 | continue 214 | 215 | # blocks until there is socket(s) ready for .recv 216 | # notice: sockets which were closed by remote, 217 | # are also regarded as read-ready by select() 218 | if self.sel: 219 | events = self.sel.select(0.5) 220 | socks_rd = tuple(key.fileobj for key, mask in events if mask & EVENT_READ) 221 | socks_wr = tuple(key.fileobj for key, mask in events if mask & EVENT_WRITE) 222 | else: 223 | r, w, _ = select.select(self.conn_rd, self.conn_wr, [], 0.5) 224 | socks_rd = tuple(r) 225 | socks_wr = tuple(w) 226 | # log.debug('socks_rd: %s, socks_wr:%s', len(socks_rd), len(socks_wr)) 227 | 228 | if not socks_rd and not self.send_buff: # reduce CPU in low traffic 229 | time.sleep(0.005) 230 | # log.debug('got rd:%s wr:%s', socks_rd, socks_wr) 231 | 232 | # ----------------- RECEIVING ---------------- 233 | # For prevent high CPU at slow network environment, we record if there is any 234 | # success network operation, if we did nothing in single loop, we'll sleep a while. 235 | _stuck_network = True 236 | 237 | for s in socks_rd: # type: socket.socket 238 | # if this socket has non-sent data, stop recving more, to prevent buff blowing up. 239 | if self.map[s] in self.send_buff: 240 | # log.debug('delay recv because another too slow %s', self.map.get(s)) 241 | continue 242 | _stuck_network = False 243 | 244 | try: 245 | received = s.recv(RECV_BUFFER_SIZE) 246 | # log.debug('recved %s from %s', len(received), s) 247 | except Exception as e: 248 | # ssl may raise SSLWantReadError or SSLWantWriteError 249 | # just continue and wait it complete 250 | if ssl and isinstance(e, (ssl.SSLWantReadError, ssl.SSLWantWriteError)): 251 | # log.warning('got %s, wait to read then', repr(e)) 252 | continue 253 | 254 | # unable to read, in most cases, it's due to socket close 255 | log.warning('error reading socket %s, %s closing', repr(e), s) 256 | self._rd_shutdown(s) 257 | continue 258 | 259 | if not received: 260 | self._rd_shutdown(s) 261 | continue 262 | else: 263 | self.send_buff[self.map[s]] = received 264 | 265 | # ----------------- SENDING ---------------- 266 | for s in socks_wr: 267 | if s not in self.send_buff: 268 | if self.map.get(s) not in self.conn_rd: 269 | self._wr_shutdown(s) 270 | continue 271 | _stuck_network = False 272 | 273 | data = self.send_buff.pop(s) 274 | try: 275 | s.send(data) 276 | # log.debug('sent %s to %s', len(data), s) 277 | except Exception as e: 278 | if ssl and isinstance(e, (ssl.SSLWantReadError, ssl.SSLWantWriteError)): 279 | # log.warning('got %s, wait to write then', repr(e)) 280 | self.send_buff[s] = data # write back for next write 281 | continue 282 | # unable to send, close connection 283 | log.warning('error sending socket %s, %s closing', repr(e), s) 284 | self._wr_shutdown(s) 285 | continue 286 | 287 | if _stuck_network: # slower at bad network 288 | time.sleep(0.001) 289 | 290 | def _sel_disable_event(self, conn, ev): 291 | try: 292 | _key = self.sel.get_key(conn) # type:selectors.SelectorKey 293 | except KeyError: 294 | pass 295 | else: 296 | if _key.events == EVENT_READ_WRITE: 297 | self.sel.modify(conn, EVENT_READ_WRITE ^ ev) 298 | else: 299 | self.sel.unregister(conn) 300 | 301 | def _rd_shutdown(self, conn, once=False): 302 | """action when connection should be read-shutdown 303 | :type conn: socket.socket 304 | """ 305 | if conn in self.conn_rd: 306 | self.conn_rd.remove(conn) 307 | if self.sel: 308 | self._sel_disable_event(conn, EVENT_READ) 309 | 310 | # if conn in self.send_buff: 311 | # del self.send_buff[conn] 312 | 313 | try: 314 | conn.shutdown(socket.SHUT_RD) 315 | except: 316 | pass 317 | 318 | if not once and conn in self.map: # use the `once` param to avoid infinite loop 319 | # if a socket is rd_shutdowned, then it's 320 | # pair should be wr_shutdown. 321 | self._wr_shutdown(self.map[conn], True) 322 | 323 | if self.map.get(conn) not in self.conn_rd: 324 | # if both two connection pair was rd-shutdowned, 325 | # this pair sockets are regarded to be completed 326 | # so we gonna close them 327 | self._terminate(conn) 328 | 329 | def _wr_shutdown(self, conn, once=False): 330 | """action when connection should be write-shutdown 331 | :type conn: socket.socket 332 | """ 333 | try: 334 | conn.shutdown(socket.SHUT_WR) 335 | except: 336 | pass 337 | 338 | if conn in self.conn_wr: 339 | self.conn_wr.remove(conn) 340 | if self.sel: 341 | self._sel_disable_event(conn, EVENT_WRITE) 342 | 343 | if not once and conn in self.map: # use the `once` param to avoid infinite loop 344 | # pair should be rd_shutdown. 345 | # if a socket is wr_shutdowned, then it's 346 | self._rd_shutdown(self.map[conn], True) 347 | 348 | def _terminate(self, conn, once=False): 349 | """terminate a sockets pair (two socket) 350 | :type conn: socket.socket 351 | :param conn: any one of the sockets pair 352 | """ 353 | try_close(conn) # close the first socket 354 | 355 | # ------ close and clean the mapped socket, if exist ------ 356 | _another_conn = self.map.pop(conn, None) 357 | 358 | self.send_buff.pop(conn, None) 359 | if self.sel: 360 | try: 361 | self.sel.unregister(conn) 362 | except: 363 | pass 364 | 365 | # ------ callback -------- 366 | # because we are not sure which socket are assigned to callback, 367 | # so we should try both 368 | if conn in self.callbacks: 369 | try: 370 | self.callbacks[conn]() 371 | except Exception as e: 372 | log.error("traceback error: {}".format(e)) 373 | log.debug(traceback.format_exc()) 374 | del self.callbacks[conn] 375 | 376 | # terminate another 377 | if not once and _another_conn in self.map: 378 | self._terminate(_another_conn) 379 | 380 | 381 | class CtrlPkg(object): 382 | """ 383 | Control Packages of shootback, not completed yet 384 | current we have: handshake and heartbeat 385 | 386 | NOTICE: If you are non-Chinese reader, 387 | please contact me for the following Chinese comment's translation 388 | http://github.com/aploium 389 | 390 | 控制包结构 总长64bytes CtrlPkg.FORMAT_PKG 391 | 使用 big-endian 392 | 393 | 体积 名称 数据类型 描述 394 | 1 pkg_ver unsigned char 包版本 *1 395 | 1 pkg_type signed char 包类型 *2 396 | 2 prgm_ver unsigned short 程序版本 *3 397 | 20 N/A N/A 预留 398 | 40 data bytes 数据区 *4 399 | 400 | *1: 包版本. 包整体结构的定义版本, 目前只有 0x01 401 | 402 | *2: 包类型. 除心跳外, 所有负数包代表由Slaver发出, 正数包由Master发出 403 | -1: Slaver-->Master 的握手响应包 PTYPE_HS_S2M 404 | 0: 心跳包 PTYPE_HEART_BEAT 405 | +1: Master-->Slaver 的握手包 PTYPE_HS_M2S 406 | 407 | *3: 默认即为 INTERNAL_VERSION 408 | 409 | *4: 数据区中的内容由各个类型的包自身定义 410 | 411 | -------------- 数据区定义 ------------------ 412 | 包类型: -1 (Slaver-->Master 的握手响应包) 413 | 体积 名称 数据类型 描述 414 | 4 crc32_s2m unsigned int 简单鉴权用 CRC32(Reversed(SECRET_KEY)) 415 | 1 ssl_flag unsigned char 是否支持SSL 416 | 其余为空 417 | *注意: -1握手包是把 SECRET_KEY 字符串翻转后取CRC32, +1握手包不预先反转 418 | 419 | 包类型: 0 (心跳) 420 | 数据区为空 421 | 422 | 包理性: +1 (Master-->Slaver 的握手包) 423 | 体积 名称 数据类型 描述 424 | 4 crc32_m2s unsigned int 简单鉴权用 CRC32(SECRET_KEY) 425 | 1 ssl_flag unsigned char 是否支持SSL 426 | 其余为空 427 | 428 | """ 429 | PACKAGE_SIZE = 2 ** 6 # 64 bytes 430 | CTRL_PKG_TIMEOUT = 5 # CtrlPkg recv timeout, in second 431 | 432 | # CRC32 for SECRET_KEY and Reversed(SECRET_KEY) 433 | # these values are set by `set_secretkey` 434 | SECRET_KEY_CRC32 = None # binascii.crc32(SECRET_KEY.encode('utf-8')) & 0xffffffff 435 | SECRET_KEY_REVERSED_CRC32 = None # binascii.crc32(SECRET_KEY[::-1].encode('utf-8')) & 0xffffffff 436 | 437 | # Package Type 438 | PTYPE_HS_S2M = -1 # handshake pkg, slaver to master 439 | PTYPE_HEART_BEAT = 0 # heart beat pkg 440 | PTYPE_HS_M2S = +1 # handshake pkg, Master to Slaver 441 | 442 | TYPE_NAME_MAP = { 443 | PTYPE_HS_S2M: "PTYPE_HS_S2M", 444 | PTYPE_HEART_BEAT: "PTYPE_HEART_BEAT", 445 | PTYPE_HS_M2S: "PTYPE_HS_M2S", 446 | } 447 | 448 | # formats 449 | # see https://docs.python.org/3/library/struct.html#format-characters 450 | # for format syntax 451 | FORMAT_PKG = b"!b b H 20x 40s" 452 | FORMATS_DATA = { 453 | PTYPE_HS_S2M: b"!I B 35x", 454 | PTYPE_HEART_BEAT: b"!40x", 455 | PTYPE_HS_M2S: b"!I B 35x", 456 | } 457 | 458 | SSL_FLAG_NONE = 0 459 | SSL_FLAG_AVAIL = 1 460 | 461 | def __init__(self, pkg_ver=0x01, pkg_type=0, 462 | prgm_ver=INTERNAL_VERSION, data=(), 463 | raw=None, 464 | ): 465 | """do not call this directly, use `CtrlPkg.pbuild_*` instead""" 466 | self.pkg_ver = pkg_ver 467 | self.pkg_type = pkg_type 468 | self.prgm_ver = prgm_ver 469 | self.data = data 470 | if raw: 471 | self.raw = raw 472 | else: 473 | self._build_bytes() 474 | 475 | @property 476 | def type_name(self): 477 | """返回人类可读的包类型""" 478 | return self.TYPE_NAME_MAP.get(self.pkg_type, "TypeUnknown") 479 | 480 | def __str__(self): 481 | return """pkg_ver: {} pkg_type:{} prgm_ver:{} data:{}""".format( 482 | self.pkg_ver, 483 | self.type_name, 484 | self.prgm_ver, 485 | self.data, 486 | ) 487 | 488 | def __repr__(self): 489 | return self.__str__() 490 | 491 | def _build_bytes(self): 492 | self.raw = struct.pack( 493 | self.FORMAT_PKG, 494 | self.pkg_ver, 495 | self.pkg_type, 496 | self.prgm_ver, 497 | self.data_encode(self.pkg_type, self.data), 498 | ) 499 | 500 | @classmethod 501 | def recalc_crc32(cls): 502 | cls.SECRET_KEY_CRC32 = binascii.crc32(SECRET_KEY.encode('utf-8')) & 0xffffffff 503 | cls.SECRET_KEY_REVERSED_CRC32 = binascii.crc32(SECRET_KEY[::-1].encode('utf-8')) & 0xffffffff 504 | 505 | @classmethod 506 | def data_decode(cls, ptype, data_raw): 507 | return struct.unpack(cls.FORMATS_DATA[ptype], data_raw) 508 | 509 | @classmethod 510 | def data_encode(cls, ptype, data): 511 | return struct.pack(cls.FORMATS_DATA[ptype], *data) 512 | 513 | def verify(self, pkg_type=None): 514 | try: 515 | if pkg_type is not None and self.pkg_type != pkg_type: 516 | return False 517 | elif self.pkg_type == self.PTYPE_HS_S2M: 518 | # Slaver-->Master 的握手响应包 519 | return self.data[0] == self.SECRET_KEY_REVERSED_CRC32 520 | 521 | elif self.pkg_type == self.PTYPE_HEART_BEAT: 522 | # 心跳 523 | return True 524 | 525 | elif self.pkg_type == self.PTYPE_HS_M2S: 526 | # Master-->Slaver 的握手包 527 | return self.data[0] == self.SECRET_KEY_CRC32 528 | 529 | else: 530 | return True 531 | except: 532 | return False 533 | 534 | @classmethod 535 | def decode_only(cls, raw): 536 | """ 537 | decode raw bytes to CtrlPkg instance, no verify 538 | use .decode_verify() if you also want verify 539 | 540 | :param raw: raw bytes content of package 541 | :type raw: bytes 542 | :rtype: CtrlPkg 543 | """ 544 | if not raw or len(raw) != cls.PACKAGE_SIZE: 545 | raise ValueError("content size should be {}, but {}".format( 546 | cls.PACKAGE_SIZE, len(raw) 547 | )) 548 | pkg_ver, pkg_type, prgm_ver, data_raw = struct.unpack(cls.FORMAT_PKG, raw) 549 | data = cls.data_decode(pkg_type, data_raw) 550 | 551 | return cls( 552 | pkg_ver=pkg_ver, pkg_type=pkg_type, 553 | prgm_ver=prgm_ver, 554 | data=data, 555 | raw=raw, 556 | ) 557 | 558 | @classmethod 559 | def decode_verify(cls, raw, pkg_type=None): 560 | """decode and verify a package 561 | :param raw: raw bytes content of package 562 | :type raw: bytes 563 | :param pkg_type: assert this package's type, 564 | if type not match, would be marked as wrong 565 | :type pkg_type: int 566 | 567 | :rtype: CtrlPkg, bool 568 | :return: tuple(CtrlPkg, is_it_a_valid_package) 569 | """ 570 | try: 571 | pkg = cls.decode_only(raw) 572 | except: 573 | log.error('unable to decode package. raw: %s', raw, exc_info=True) 574 | return None, False 575 | else: 576 | return pkg, pkg.verify(pkg_type=pkg_type) 577 | 578 | @classmethod 579 | def pbuild_hs_m2s(cls, ssl_avail=False): 580 | """pkg build: Handshake Master to Slaver 581 | """ 582 | ssl_flag = cls.SSL_FLAG_AVAIL if ssl_avail else cls.SSL_FLAG_NONE 583 | return cls( 584 | pkg_type=cls.PTYPE_HS_M2S, 585 | data=(cls.SECRET_KEY_CRC32, ssl_flag), 586 | ) 587 | 588 | @classmethod 589 | def pbuild_hs_s2m(cls, ssl_avail=False): 590 | """pkg build: Handshake Slaver to Master""" 591 | ssl_flag = cls.SSL_FLAG_AVAIL if ssl_avail else cls.SSL_FLAG_NONE 592 | return cls( 593 | pkg_type=cls.PTYPE_HS_S2M, 594 | data=(cls.SECRET_KEY_REVERSED_CRC32, ssl_flag), 595 | ) 596 | 597 | @classmethod 598 | def pbuild_heart_beat(cls): 599 | """pkg build: Heart Beat Package""" 600 | return cls( 601 | pkg_type=cls.PTYPE_HEART_BEAT, 602 | ) 603 | 604 | @classmethod 605 | def recv(cls, sock, timeout=CTRL_PKG_TIMEOUT, expect_ptype=None): 606 | """just a shortcut function 607 | :param sock: which socket to recv CtrlPkg from 608 | :type sock: socket.socket 609 | :rtype: CtrlPkg,bool 610 | """ 611 | buff = select_recv(sock, cls.PACKAGE_SIZE, timeout) 612 | pkg, verify = CtrlPkg.decode_verify(buff, pkg_type=expect_ptype) # type: CtrlPkg,bool 613 | return pkg, verify 614 | -------------------------------------------------------------------------------- /master.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | import os 4 | import tempfile 5 | import queue 6 | import atexit 7 | from common_func import * 8 | 9 | _listening_sockets = [] # for close at exit 10 | __author__ = "Aploium " 11 | __website__ = "https://github.com/aploium/shootback" 12 | 13 | _DEFAULT_SSL_KEY = '''\ 14 | -----BEGIN PRIVATE KEY----- 15 | MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMqPYWmoQJaHwu3/ 16 | 0Q5R8RFpOzFwys/5xgypGMbFmW0oSpiVMA0u+lakxb7Z46RU/Y/kV4XUjB9+uDcI 17 | FlU520jWJVZ9g09gMKaPyDjxdKueKl2KDfn7i4unBQwouqzipPWXv0L8RnHF1gzG 18 | XgN/Dh6+5ZoY53JvUZaFbQhjjtkjAgMBAAECgYB8WRjL6+X6gs0/ndOQnu0GaztT 19 | VpKqqgLSstvq6lMNl7ZzhOJCtZwopG5ggxIkR6iBNQQlvB1pGDmuTuCm4SWjsXsS 20 | x2iDvPTlx9Y1ke9SWszZ6mOvumeHLnnxU0tp+ECySphQN5XxzH2yHzeXVTWpIim6 21 | ADFrtbltaPFLghZbQQJBAOT0tU+QLtlaKit6iY1QrZntsVzeCZYxh2ktz4Hsk8RN 22 | K4U4FCoWz3rJrkj1gFIrgoSgQ6+VzjXzFDbKJeP7b38CQQDifIBhi9JAVKlPgklG 23 | e595EYa3J4a601AAIxYVVwyfn8CiUUHCHKM+roJywh0uvKVXN6sn1Wi4zU8H8BQ1 24 | 9KhdAkAnIY/PfmQTb+6fKb1SssRI97AFoElhKyvqlRLPMOD8fvf+N9xyaR2i7c9k 25 | 1tjMsnUHN+D5pI/u9pGw35HkSjf/AkB4rpCV6bQhtTr2c9zpoqvKDi2zYGtpF3oU 26 | aJ22x0ihsbUqiJO6hBn0J3a5AXgdVEXh4Hbh5dRETJnlB+ctDO29AkBXDgUXbltm 27 | CRtey5ZwnlAKI8jwx3q4jaldTCJrwThfgNdlvWPnKjPhIYG/OogLAm82grruSzOQ 28 | a2xDsGiXCZoM 29 | -----END PRIVATE KEY----- 30 | ''' 31 | _DEFAULT_SSL_CERT = '''\ 32 | -----BEGIN CERTIFICATE----- 33 | MIICmzCCAgSgAwIBAgIJANe4OK2h+u9iMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNV 34 | BAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMS0wKwYDVQQKDCRodHRwczovL2dp 35 | dGh1Yi5jb20vYXBsb2l1bS9zaG9vdGJhY2sxEjAQBgNVBAMMCWxvY2FsaG9zdDAe 36 | Fw0xODEwMjQxNjA5MDVaFw0yODEwMjExNjA5MDVaMGUxCzAJBgNVBAYTAlVTMRMw 37 | EQYDVQQIDApTb21lLVN0YXRlMS0wKwYDVQQKDCRodHRwczovL2dpdGh1Yi5jb20v 38 | YXBsb2l1bS9zaG9vdGJhY2sxEjAQBgNVBAMMCWxvY2FsaG9zdDCBnzANBgkqhkiG 39 | 9w0BAQEFAAOBjQAwgYkCgYEAyo9haahAlofC7f/RDlHxEWk7MXDKz/nGDKkYxsWZ 40 | bShKmJUwDS76VqTFvtnjpFT9j+RXhdSMH364NwgWVTnbSNYlVn2DT2Awpo/IOPF0 41 | q54qXYoN+fuLi6cFDCi6rOKk9Ze/QvxGccXWDMZeA38OHr7lmhjncm9RloVtCGOO 42 | 2SMCAwEAAaNTMFEwHQYDVR0OBBYEFEJESWZQ80wg2+1HmLW8nFtYyg9NMB8GA1Ud 43 | IwQYMBaAFEJESWZQ80wg2+1HmLW8nFtYyg9NMA8GA1UdEwEB/wQFMAMBAf8wDQYJ 44 | KoZIhvcNAQELBQADgYEAJD3ZWeLJaxWyCVSsKSxFZfTyYMhKSI9UjssDU48ENbfz 45 | hTdU2KxHFmRlTIdl02BcvpjiCHOCYxGkSBXctpXAHp9oU8fpzNIdekmgmZD2GjHS 46 | NbE3PCsO3NNtnc3Oj96HCww2MeC0ro9j0uDkyl+0zx47UeFUDqqSFJZ3bQa8j0w= 47 | -----END CERTIFICATE----- 48 | ''' 49 | 50 | 51 | @atexit.register 52 | def close_listening_socket_at_exit(): 53 | log.info("exiting...") 54 | for s in _listening_sockets: 55 | log.info("closing: {}".format(s)) 56 | try_close(s) 57 | 58 | 59 | def try_bind_port(sock, addr): 60 | while True: 61 | try: 62 | sock.bind(addr) 63 | except Exception as e: 64 | log.error(("unable to bind {}, {}. If this port was used by the recently-closed shootback itself\n" 65 | "then don't worry, it would be available in several seconds\n" 66 | "we'll keep trying....").format(addr, e)) 67 | log.debug(traceback.format_exc()) 68 | time.sleep(3) 69 | else: 70 | break 71 | 72 | 73 | class Master(object): 74 | def __init__(self, customer_listen_addr, communicate_addr=None, 75 | slaver_pool=None, working_pool=None, 76 | ssl=False 77 | ): 78 | """ 79 | 80 | :param customer_listen_addr: equals to the -c/--customer param 81 | :param communicate_addr: equals to the -m/--master param 82 | """ 83 | self.thread_pool = {} 84 | self.thread_pool["spare_slaver"] = {} 85 | self.thread_pool["working_slaver"] = {} 86 | 87 | self.working_pool = working_pool or {} 88 | 89 | self.socket_bridge = SocketBridge() 90 | 91 | # a queue for customers who have connected to us, 92 | # but not assigned a slaver yet 93 | self.pending_customers = queue.Queue() 94 | 95 | if ssl: 96 | self.ssl_context = self._make_ssl_context() 97 | self.ssl_avail = self.ssl_context is not None 98 | else: 99 | self.ssl_avail = False 100 | self.ssl_context = None 101 | 102 | self.communicate_addr = communicate_addr 103 | 104 | _fmt_communicate_addr = fmt_addr(self.communicate_addr) 105 | 106 | if slaver_pool: 107 | # 若使用外部slaver_pool, 就不再初始化listen 108 | # 这是以后待添加的功能 109 | self.external_slaver = True 110 | self.thread_pool["listen_slaver"] = None 111 | else: 112 | # 自己listen来获取slaver 113 | self.external_slaver = False 114 | self.slaver_pool = collections.deque() 115 | # prepare Thread obj, not activated yet 116 | self.thread_pool["listen_slaver"] = threading.Thread( 117 | target=self._listen_slaver, 118 | name="listen_slaver-{}".format(_fmt_communicate_addr), 119 | daemon=True, 120 | ) 121 | 122 | # prepare Thread obj, not activated yet 123 | self.customer_listen_addr = customer_listen_addr 124 | self.thread_pool["listen_customer"] = threading.Thread( 125 | target=self._listen_customer, 126 | name="listen_customer-{}".format(_fmt_communicate_addr), 127 | daemon=True, 128 | ) 129 | 130 | # prepare Thread obj, not activated yet 131 | self.thread_pool["heart_beat_daemon"] = threading.Thread( 132 | target=self._heart_beat_daemon, 133 | name="heart_beat_daemon-{}".format(_fmt_communicate_addr), 134 | daemon=True, 135 | ) 136 | 137 | # prepare assign_slaver_daemon 138 | self.thread_pool["assign_slaver_daemon"] = threading.Thread( 139 | target=self._assign_slaver_daemon, 140 | name="assign_slaver_daemon-{}".format(_fmt_communicate_addr), 141 | daemon=True, 142 | ) 143 | 144 | def serve_forever(self): 145 | if not self.external_slaver: 146 | self.thread_pool["listen_slaver"].start() 147 | self.thread_pool["heart_beat_daemon"].start() 148 | self.thread_pool["listen_customer"].start() 149 | self.thread_pool["assign_slaver_daemon"].start() 150 | self.thread_pool["socket_bridge"] = self.socket_bridge.start_as_daemon() 151 | 152 | while True: 153 | time.sleep(10) 154 | 155 | def _make_ssl_context(self): 156 | if ssl is None: 157 | log.warning('ssl module is NOT valid in this machine! Fallback to plain') 158 | return None 159 | 160 | ctx = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) 161 | ctx.check_hostname = False 162 | ctx.load_default_certs(ssl.Purpose.SERVER_AUTH) 163 | ctx.verify_mode = ssl.CERT_NONE 164 | 165 | _certfile = tempfile.mktemp() 166 | with open(_certfile, 'w') as fw: 167 | fw.write(_DEFAULT_SSL_CERT) 168 | _keyfile = tempfile.mktemp() 169 | with open(_keyfile, 'w') as fw: 170 | fw.write(_DEFAULT_SSL_KEY) 171 | ctx.load_cert_chain(_certfile, _keyfile) 172 | os.remove(_certfile) 173 | os.remove(_keyfile) 174 | 175 | return ctx 176 | 177 | def _transfer_complete(self, addr_customer): 178 | """a callback for SocketBridge, do some cleanup jobs""" 179 | log.info("customer complete: {}".format(addr_customer)) 180 | del self.working_pool[addr_customer] 181 | 182 | def _serve_customer(self, conn_customer, conn_slaver): 183 | """put customer and slaver sockets into SocketBridge, let them exchange data""" 184 | self.socket_bridge.add_conn_pair( 185 | conn_customer, conn_slaver, 186 | functools.partial( # it's a callback 187 | # 这个回调用来在传输完成后删除工作池中对应记录 188 | self._transfer_complete, 189 | conn_customer.getpeername() 190 | ) 191 | ) 192 | 193 | @staticmethod 194 | def _send_heartbeat(conn_slaver): 195 | """send and verify heartbeat pkg""" 196 | conn_slaver.send(CtrlPkg.pbuild_heart_beat().raw) 197 | 198 | pkg, verify = CtrlPkg.recv( 199 | conn_slaver, expect_ptype=CtrlPkg.PTYPE_HEART_BEAT) # type: CtrlPkg,bool 200 | 201 | if not verify: 202 | return False 203 | 204 | if pkg.prgm_ver < 0x000B: 205 | # shootback before 2.2.5-r10 use two-way heartbeat 206 | # so there is no third pkg to send 207 | pass 208 | else: 209 | # newer version use TCP-like 3-way heartbeat 210 | # the older 2-way heartbeat can't only ensure the 211 | # master --> slaver pathway is OK, but the reverse 212 | # communicate may down. So we need a TCP-like 3-way 213 | # heartbeat 214 | conn_slaver.send(CtrlPkg.pbuild_heart_beat().raw) 215 | 216 | return verify 217 | 218 | def _heart_beat_daemon(self): 219 | """ 220 | 221 | 每次取出slaver队列头部的一个, 测试心跳, 并把它放回尾部. 222 | slaver若超过 SPARE_SLAVER_TTL 秒未收到心跳, 则会自动重连 223 | 所以睡眠间隔(delay)满足 delay * slaver总数 < TTL 224 | 使得一轮循环的时间小于TTL, 225 | 保证每个slaver都在过期前能被心跳保活 226 | """ 227 | default_delay = 5 + SPARE_SLAVER_TTL // 12 228 | delay = default_delay 229 | log.info("heart beat daemon start, delay: {}s".format(delay)) 230 | while True: 231 | time.sleep(delay) 232 | # log.debug("heart_beat_daemon: hello! im weak") 233 | 234 | # ---------------------- preparation ----------------------- 235 | slaver_count = len(self.slaver_pool) 236 | if not slaver_count: 237 | log.warning("heart_beat_daemon: sorry, no slaver available, keep sleeping") 238 | # restore default delay if there is no slaver 239 | delay = default_delay 240 | continue 241 | else: 242 | # notice this `slaver_count*2 + 1` 243 | # slaver will expire and re-connect if didn't receive 244 | # heartbeat pkg after SPARE_SLAVER_TTL seconds. 245 | # set delay to be short enough to let every slaver receive heartbeat 246 | # before expire 247 | delay = 1 + SPARE_SLAVER_TTL // max(slaver_count * 2 + 1, 12) 248 | 249 | # pop the oldest slaver 250 | # heartbeat it and then put it to the end of queue 251 | slaver = self.slaver_pool.popleft() 252 | addr_slaver = slaver["addr_slaver"] 253 | 254 | # ------------------ real heartbeat begin -------------------- 255 | start_time = time.perf_counter() 256 | try: 257 | hb_result = self._send_heartbeat(slaver["conn_slaver"]) 258 | except Exception as e: 259 | log.warning("error during heartbeat to {}: {}".format( 260 | fmt_addr(addr_slaver), e)) 261 | log.debug(traceback.format_exc()) 262 | hb_result = False 263 | finally: 264 | time_used = round((time.perf_counter() - start_time) * 1000.0, 2) 265 | # ------------------ real heartbeat end ---------------------- 266 | 267 | if not hb_result: 268 | log.warning("heart beat failed: {}, time: {}ms".format( 269 | fmt_addr(addr_slaver), time_used)) 270 | try_close(slaver["conn_slaver"]) 271 | del slaver["conn_slaver"] 272 | 273 | # if heartbeat failed, start the next heartbeat immediately 274 | # because in most cases, all 5 slaver connection will 275 | # fall and re-connect in the same time 276 | delay = 0 277 | 278 | else: 279 | log.debug("heartbeat success: {}, time: {}ms".format( 280 | fmt_addr(addr_slaver), time_used)) 281 | self.slaver_pool.append(slaver) 282 | 283 | def _handshake(self, conn_slaver): 284 | """ 285 | handshake before real data transfer 286 | it ensures: 287 | 1. client is alive and ready for transmission 288 | 2. client is shootback_slaver, not mistakenly connected other program 289 | 3. verify the SECRET_KEY, establish SSL 290 | 4. tell slaver it's time to connect target 291 | 292 | handshake procedure: 293 | 1. master hello --> slaver 294 | 2. slaver verify master's hello 295 | 3. slaver hello --> master 296 | 4. (immediately after 3) slaver connect to target 297 | 4. master verify slaver 298 | 5. [optional] establish SSL 299 | 6. enter real data transfer 300 | 301 | Args: 302 | conn_slaver (socket.socket) 303 | Return: 304 | socket.socket|ssl.SSLSocket: socket obj(may be ssl-socket) if handshake success, else None 305 | """ 306 | conn_slaver.send(CtrlPkg.pbuild_hs_m2s(ssl_avail=self.ssl_avail).raw) 307 | 308 | buff = select_recv(conn_slaver, CtrlPkg.PACKAGE_SIZE, 2) 309 | if buff is None: 310 | return None 311 | 312 | pkg, correct = CtrlPkg.decode_verify(buff, CtrlPkg.PTYPE_HS_S2M) # type: CtrlPkg,bool 313 | 314 | if not correct: 315 | return None 316 | 317 | if not self.ssl_avail or pkg.data[1] == CtrlPkg.SSL_FLAG_NONE: 318 | if self.ssl_avail: 319 | log.warning('client %s not enabled SSL, fallback to plain.', conn_slaver.getpeername()) 320 | return conn_slaver 321 | else: 322 | ssl_conn_slaver = self.ssl_context.wrap_socket(conn_slaver, server_side=True) # type: ssl.SSLSocket 323 | log.debug('ssl established slaver: %s', ssl_conn_slaver.getpeername()) 324 | return ssl_conn_slaver 325 | 326 | def _get_an_active_slaver(self): 327 | """get and activate an slaver for data transfer""" 328 | try_count = 100 329 | while True: 330 | if not try_count: 331 | return None 332 | try: 333 | dict_slaver = self.slaver_pool.popleft() 334 | except: 335 | time.sleep(0.02) 336 | try_count -= 1 337 | if try_count % 10 == 0: 338 | log.error("!!NO SLAVER AVAILABLE!! trying {}".format(try_count)) 339 | continue 340 | 341 | conn_slaver = dict_slaver["conn_slaver"] 342 | 343 | try: 344 | # this returned conn may be ssl-socket or plain socket 345 | actual_conn = self._handshake(conn_slaver) 346 | except Exception as e: 347 | log.warning("Handshake failed. %s %s", dict_slaver["addr_slaver"], e) 348 | log.debug(traceback.format_exc()) 349 | actual_conn = None 350 | 351 | try_count -= 1 352 | if try_count % 10 == 0: 353 | log.error("!!NO SLAVER AVAILABLE!! trying {}".format(try_count)) 354 | 355 | if actual_conn is not None: 356 | return actual_conn 357 | else: 358 | log.warning("slaver handshake failed: %s", dict_slaver["addr_slaver"]) 359 | try_close(conn_slaver) 360 | 361 | time.sleep(0.02) 362 | 363 | def _assign_slaver_daemon(self): 364 | """assign slaver for customer""" 365 | while True: 366 | # get a newly connected customer 367 | conn_customer, addr_customer = self.pending_customers.get() 368 | 369 | try: 370 | conn_slaver = self._get_an_active_slaver() 371 | except: 372 | log.error('error in getting slaver', exc_info=True) 373 | continue 374 | if conn_slaver is None: 375 | log.warning("Closing customer[%s] because no available slaver found", addr_customer) 376 | try_close(conn_customer) 377 | 378 | continue 379 | else: 380 | log.debug("Using slaver: %s for %s", conn_slaver.getpeername(), addr_customer) 381 | 382 | self.working_pool[addr_customer] = { 383 | "addr_customer": addr_customer, 384 | "conn_customer": conn_customer, 385 | "conn_slaver": conn_slaver, 386 | } 387 | 388 | try: 389 | self._serve_customer(conn_customer, conn_slaver) 390 | except: 391 | log.error('error adding to socket_bridge', exc_info=True) 392 | try_close(conn_customer) 393 | try_close(conn_slaver) 394 | continue 395 | 396 | def _listen_slaver(self): 397 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 398 | try_bind_port(sock, self.communicate_addr) 399 | sock.listen(10) 400 | _listening_sockets.append(sock) 401 | log.info("Listening for slavers: {}".format( 402 | fmt_addr(self.communicate_addr))) 403 | while True: 404 | conn, addr = sock.accept() 405 | self.slaver_pool.append({ 406 | "addr_slaver": addr, 407 | "conn_slaver": conn, 408 | }) 409 | log.info("Got slaver {} Total: {}".format( 410 | fmt_addr(addr), len(self.slaver_pool) 411 | )) 412 | 413 | def _listen_customer(self): 414 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 415 | try_bind_port(sock, self.customer_listen_addr) 416 | sock.listen(20) 417 | _listening_sockets.append(sock) 418 | log.info("Listening for customers: {}".format( 419 | fmt_addr(self.customer_listen_addr))) 420 | while True: 421 | conn_customer, addr_customer = sock.accept() 422 | log.info("Serving customer: {} Total customers: {}".format( 423 | addr_customer, self.pending_customers.qsize() + 1 424 | )) 425 | 426 | # just put it into the queue, 427 | # let _assign_slaver_daemon() do the else 428 | # don't block this loop 429 | self.pending_customers.put((conn_customer, addr_customer)) 430 | 431 | 432 | def run_master(communicate_addr, customer_listen_addr, ssl=False): 433 | log.info("shootback {} running as master".format(version_info())) 434 | log.info("author: {} site: {}".format(__author__, __website__)) 435 | log.info("slaver from: {} customer from: {}".format( 436 | fmt_addr(communicate_addr), fmt_addr(customer_listen_addr))) 437 | 438 | Master(customer_listen_addr, communicate_addr, ssl=ssl).serve_forever() 439 | 440 | 441 | def argparse_master(): 442 | import argparse 443 | parser = argparse.ArgumentParser( 444 | description="""shootback (master) {ver} 445 | A fast and reliable reverse TCP tunnel. (this is master) 446 | Help access local-network service from Internet. 447 | https://github.com/aploium/shootback""".format(ver=version_info()), 448 | epilog=""" 449 | Example1: 450 | tunnel local ssh to public internet, assume master's ip is 1.2.3.4 451 | Master(this pc): master.py -m 0.0.0.0:10000 -c 0.0.0.0:10022 452 | Slaver(another private pc): slaver.py -m 1.2.3.4:10000 -t 127.0.0.1:22 453 | Customer(any internet user): ssh 1.2.3.4 -p 10022 454 | the actual traffic is: customer <--> master(1.2.3.4 this pc) <--> slaver(private network) <--> ssh(private network) 455 | 456 | Example2: 457 | Tunneling for www.example.com 458 | Master(this pc): master.py -m 127.0.0.1:10000 -c 127.0.0.1:10080 459 | Slaver(this pc): slaver.py -m 127.0.0.1:10000 -t example.com:80 460 | Customer(this pc): curl -v -H "host: example.com" 127.0.0.1:10080 461 | 462 | Tips: ANY service using TCP is shootback-able. HTTP/FTP/Proxy/SSH/VNC/... 463 | """, 464 | formatter_class=argparse.RawDescriptionHelpFormatter 465 | ) 466 | parser.add_argument("-m", "--master", required=True, 467 | metavar="host:port", 468 | help="listening for slavers, usually an Public-Internet-IP. Slaver comes in here eg: 2.3.3.3:10000") 469 | parser.add_argument("-c", "--customer", required=True, 470 | metavar="host:port", 471 | help="listening for customers, 3rd party program connects here eg: 10.1.2.3:10022") 472 | parser.add_argument("-k", "--secretkey", default="shootback", 473 | help="secretkey to identity master and slaver, should be set to the same value in both side") 474 | parser.add_argument("-v", "--verbose", action="count", default=0, 475 | help="verbose output") 476 | parser.add_argument("-q", "--quiet", action="count", default=0, 477 | help="quiet output, only display warning and errors, use two to disable output") 478 | parser.add_argument("-V", "--version", action="version", version="shootback {}-master".format(version_info())) 479 | parser.add_argument("--ttl", default=300, type=int, dest="SPARE_SLAVER_TTL", 480 | help="standing-by slaver's TTL, default is 300. " 481 | "In master side, this value affects heart-beat frequency. " 482 | "Default value is optimized for most cases") 483 | parser.add_argument('--ssl', action='store_true', help='[experimental] try using ssl for data encryption. ' 484 | 'It may be enabled by default in future version') 485 | 486 | return parser.parse_args() 487 | 488 | 489 | def main_master(): 490 | global SPARE_SLAVER_TTL 491 | 492 | args = argparse_master() 493 | 494 | if args.verbose and args.quiet: 495 | print("-v and -q should not appear together") 496 | exit(1) 497 | 498 | communicate_addr = split_host(args.master) 499 | customer_listen_addr = split_host(args.customer) 500 | 501 | set_secretkey(args.secretkey) 502 | 503 | SPARE_SLAVER_TTL = args.SPARE_SLAVER_TTL 504 | if args.quiet < 2: 505 | if args.verbose: 506 | level = logging.DEBUG 507 | elif args.quiet: 508 | level = logging.WARNING 509 | else: 510 | level = logging.INFO 511 | configure_logging(level) 512 | 513 | run_master(communicate_addr, customer_listen_addr, ssl=args.ssl) 514 | 515 | 516 | if __name__ == '__main__': 517 | main_master() 518 | -------------------------------------------------------------------------------- /slaver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding=utf-8 3 | from __future__ import print_function, unicode_literals, division, absolute_import 4 | 5 | from common_func import * 6 | 7 | __author__ = "Aploium " 8 | __website__ = "https://github.com/aploium/shootback" 9 | 10 | 11 | class Slaver(object): 12 | """ 13 | slaver socket阶段 14 | 连接master->等待->心跳(重复)--->握手-->正式传输数据->退出 15 | """ 16 | 17 | def __init__(self, communicate_addr, target_addr, max_spare_count=5, ssl=False): 18 | self.communicate_addr = communicate_addr 19 | self.target_addr = target_addr 20 | self.max_spare_count = max_spare_count 21 | 22 | self.spare_slaver_pool = {} 23 | self.working_pool = {} 24 | self.socket_bridge = SocketBridge() 25 | 26 | if ssl: 27 | self.ssl_context = self._make_ssl_context() 28 | self.ssl_avail = self.ssl_context is not None 29 | else: 30 | self.ssl_avail = False 31 | self.ssl_context = None 32 | 33 | def _make_ssl_context(self): 34 | if ssl is None: 35 | log.warning('ssl module is NOT valid in this machine! Fallback to plain') 36 | return None 37 | 38 | ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) 39 | ctx.check_hostname = False 40 | ctx.load_default_certs(ssl.Purpose.CLIENT_AUTH) 41 | ctx.verify_mode = ssl.CERT_NONE 42 | 43 | return ctx 44 | 45 | def _connect_master(self): 46 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 47 | sock.connect(self.communicate_addr) 48 | 49 | self.spare_slaver_pool[sock.getsockname()] = { 50 | "conn_slaver": sock, 51 | } 52 | 53 | return sock 54 | 55 | def _connect_target(self): 56 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 57 | sock.connect(self.target_addr) 58 | 59 | log.debug("connected to target[{}] at: {}".format( 60 | sock.getpeername(), 61 | sock.getsockname(), 62 | )) 63 | 64 | return sock 65 | 66 | def _response_heartbeat(self, conn_slaver, hb_from_master): 67 | # assert isinstance(hb_from_master, CtrlPkg) 68 | # assert isinstance(conn_slaver, socket.SocketType) 69 | if hb_from_master.prgm_ver < 0x000B: 70 | # shootback before 2.2.5-r10 use two-way heartbeat 71 | # so just send a heart_beat pkg back 72 | conn_slaver.send(CtrlPkg.pbuild_heart_beat().raw) 73 | return True 74 | else: 75 | # newer version use TCP-like 3-way heartbeat 76 | # the older 2-way heartbeat can't only ensure the 77 | # master --> slaver pathway is OK, but the reverse 78 | # communicate may down. So we need a TCP-like 3-way 79 | # heartbeat 80 | conn_slaver.send(CtrlPkg.pbuild_heart_beat().raw) 81 | pkg, verify = CtrlPkg.recv( 82 | conn_slaver, 83 | expect_ptype=CtrlPkg.PTYPE_HEART_BEAT) # type: CtrlPkg,bool 84 | if verify: 85 | log.debug("heartbeat success {}".format( 86 | fmt_addr(conn_slaver.getsockname()))) 87 | return True 88 | else: 89 | log.warning( 90 | "received a wrong pkg[{}] during heartbeat, {}".format( 91 | pkg, conn_slaver.getsockname() 92 | )) 93 | return False 94 | 95 | def _stage_ctrlpkg(self, conn_slaver): 96 | """ 97 | handling CtrlPkg until handshake 98 | 99 | well, there is only one CtrlPkg: heartbeat, yet 100 | 101 | it ensures: 102 | 1. network is ok, master is alive 103 | 2. master is shootback_master, not bad guy 104 | 3. verify the SECRET_KEY 105 | 4. tell slaver it's time to connect target 106 | 107 | handshake procedure: 108 | 1. master hello --> slaver 109 | 2. slaver verify master's hello 110 | 3. slaver hello --> master 111 | 4. (immediately after 3) slaver connect to target 112 | 4. master verify slaver 113 | 5. enter real data transfer 114 | """ 115 | while True: # 可能会有一段时间的心跳包 116 | 117 | # recv master --> slaver 118 | # timeout is set to `SPARE_SLAVER_TTL` 119 | # which means if not receive pkg from master in SPARE_SLAVER_TTL seconds, 120 | # this connection would expire and re-connect 121 | pkg, correct = CtrlPkg.recv(conn_slaver, SPARE_SLAVER_TTL) # type: CtrlPkg,bool 122 | 123 | if not correct: 124 | return None 125 | 126 | log.debug("CtrlPkg from {}: {}".format(conn_slaver.getpeername(), pkg)) 127 | 128 | if pkg.pkg_type == CtrlPkg.PTYPE_HEART_BEAT: 129 | # if the pkg is heartbeat pkg, enter handshake procedure 130 | if not self._response_heartbeat(conn_slaver, pkg): 131 | return None 132 | 133 | elif pkg.pkg_type == CtrlPkg.PTYPE_HS_M2S: 134 | # 拿到了开始传输的握手包, 进入工作阶段 135 | 136 | break 137 | 138 | # send slaver hello --> master 139 | actual_conn = self._response_handshake(conn_slaver, pkg) 140 | 141 | return actual_conn 142 | 143 | def _response_handshake(self, conn_slaver, handshake_pkg): 144 | """ 145 | response master's handshake 146 | check if ssl is avail, if avail, establish ssl 147 | 148 | Args: 149 | conn_slaver (socket.socket): 150 | handshake_pkg (CtrlPkg): 151 | 152 | Returns: 153 | socket.socket|ssl.SSLSocket 154 | """ 155 | conn_slaver.send(CtrlPkg.pbuild_hs_s2m(ssl_avail=self.ssl_avail).raw) 156 | 157 | if not self.ssl_avail or handshake_pkg.data[1] == CtrlPkg.SSL_FLAG_NONE: 158 | if self.ssl_avail: 159 | log.warning('master %s does not enabled SSL, fallback to plain', conn_slaver.getpeername()) 160 | return conn_slaver 161 | else: 162 | ssl_conn_slaver = self.ssl_context.wrap_socket(conn_slaver, server_side=False) # type: ssl.SSLSocket 163 | log.debug('ssl established slaver: %s', ssl_conn_slaver.getpeername()) 164 | return ssl_conn_slaver 165 | 166 | def _transfer_complete(self, addr_slaver): 167 | """a callback for SocketBridge, do some cleanup jobs""" 168 | pair = self.working_pool.pop(addr_slaver) 169 | try_close(pair['conn_slaver']) 170 | try_close(pair['conn_target']) 171 | log.info("slaver complete: {}".format(addr_slaver)) 172 | 173 | def _slaver_working(self, conn_slaver): 174 | addr_slaver = conn_slaver.getsockname() 175 | addr_master = conn_slaver.getpeername() 176 | 177 | # --------- handling CtrlPkg until handshake ------------- 178 | try: 179 | actual_conn = self._stage_ctrlpkg(conn_slaver) 180 | except Exception as e: 181 | log.warning("slaver{} waiting handshake failed {}".format( 182 | fmt_addr(addr_slaver), e)) 183 | log.debug(traceback.print_exc()) 184 | actual_conn = None 185 | else: 186 | if actual_conn is None: 187 | log.warning("bad handshake or timeout between: {} and {}".format( 188 | fmt_addr(addr_master), fmt_addr(addr_slaver))) 189 | 190 | if actual_conn is None: 191 | # handshake failed or timeout 192 | del self.spare_slaver_pool[addr_slaver] 193 | try_close(conn_slaver) 194 | 195 | log.warning("a slaver[{}] abort due to handshake error or timeout".format( 196 | fmt_addr(addr_slaver))) 197 | return 198 | else: 199 | log.info("Success master handshake from: {} to {}".format( 200 | fmt_addr(addr_master), fmt_addr(addr_slaver))) 201 | 202 | # ----------- slaver activated! ------------ 203 | # move self from spare_slaver_pool to working_pool 204 | self.working_pool[addr_slaver] = self.spare_slaver_pool.pop(addr_slaver) 205 | self.working_pool[addr_slaver]['conn_slaver'] = actual_conn 206 | 207 | # ----------- connecting to target ---------- 208 | try: 209 | conn_target = self._connect_target() 210 | except: 211 | log.error("unable to connect target") 212 | try_close(actual_conn) 213 | 214 | del self.working_pool[addr_slaver] 215 | return 216 | self.working_pool[addr_slaver]["conn_target"] = conn_target 217 | 218 | # ----------- all preparation finished ----------- 219 | # pass two sockets to SocketBridge, and let it do the 220 | # real data exchange task 221 | try: 222 | self.socket_bridge.add_conn_pair( 223 | actual_conn, conn_target, 224 | functools.partial( 225 | # 这个回调用来在传输完成后删除工作池中对应记录 226 | self._transfer_complete, addr_slaver 227 | ) 228 | ) 229 | except: 230 | log.error('error adding to socket_bridge', exc_info=True) 231 | try_close(actual_conn) 232 | try_close(conn_target) 233 | 234 | # this slaver thread exits here 235 | return 236 | 237 | def serve_forever(self): 238 | self.socket_bridge.start_as_daemon() # hi, don't ignore me 239 | 240 | # sleep between two retries if exception occurs 241 | # eg: master down or network temporary failed 242 | # err_delay would increase if err occurs repeatedly 243 | # until `max_err_delay` 244 | # would immediately decrease to 0 after a success connection 245 | err_delay = 0 246 | max_err_delay = 15 247 | # spare_delay is sleep cycle if we are full of spare slaver 248 | # would immediately decrease to 0 after a slaver lack 249 | spare_delay = 0.08 250 | default_spare_delay = 0.08 251 | 252 | while True: 253 | if len(self.spare_slaver_pool) >= self.max_spare_count: 254 | time.sleep(spare_delay) 255 | spare_delay = (spare_delay + default_spare_delay) / 2.0 256 | continue 257 | else: 258 | spare_delay = 0.0 259 | 260 | try: 261 | conn_slaver = self._connect_master() 262 | except Exception as e: 263 | log.warning("unable to connect master {}".format(e), exc_info=True) 264 | time.sleep(err_delay) 265 | if err_delay < max_err_delay: 266 | err_delay += 1 267 | continue 268 | 269 | try: 270 | t = threading.Thread(target=self._slaver_working, 271 | args=(conn_slaver,) 272 | ) 273 | t.daemon = True 274 | t.start() 275 | 276 | log.info("connected to master[{}] at {} total: {}".format( 277 | fmt_addr(conn_slaver.getpeername()), 278 | fmt_addr(conn_slaver.getsockname()), 279 | len(self.spare_slaver_pool), 280 | )) 281 | except Exception as e: 282 | log.error("unable create Thread: {}".format(e)) 283 | log.debug(traceback.format_exc()) 284 | time.sleep(err_delay) 285 | 286 | if err_delay < max_err_delay: 287 | err_delay += 1 288 | continue 289 | 290 | # set err_delay if everything is ok 291 | err_delay = 0 292 | 293 | 294 | def run_slaver(communicate_addr, target_addr, max_spare_count=5, ssl=False): 295 | log.info("running as slaver, master addr: {} target: {}".format( 296 | fmt_addr(communicate_addr), fmt_addr(target_addr) 297 | )) 298 | 299 | Slaver(communicate_addr, target_addr, 300 | max_spare_count=max_spare_count, 301 | ssl=ssl, 302 | ).serve_forever() 303 | 304 | 305 | def argparse_slaver(): 306 | import argparse 307 | parser = argparse.ArgumentParser( 308 | description="""shootback {ver}-slaver 309 | A fast and reliable reverse TCP tunnel (this is slaver) 310 | Help access local-network service from Internet. 311 | https://github.com/aploium/shootback""".format(ver=version_info()), 312 | epilog=""" 313 | Example1: 314 | tunnel local ssh to public internet, assume master's ip is 1.2.3.4 315 | Master(another public server): master.py -m 0.0.0.0:10000 -c 0.0.0.0:10022 316 | Slaver(this pc): slaver.py -m 1.2.3.4:10000 -t 127.0.0.1:22 317 | Customer(any internet user): ssh 1.2.3.4 -p 10022 318 | the actual traffic is: customer <--> master(1.2.3.4) <--> slaver(this pc) <--> ssh(this pc) 319 | 320 | Example2: 321 | Tunneling for www.example.com 322 | Master(this pc): master.py -m 127.0.0.1:10000 -c 127.0.0.1:10080 323 | Slaver(this pc): slaver.py -m 127.0.0.1:10000 -t example.com:80 324 | Customer(this pc): curl -v -H "host: example.com" 127.0.0.1:10080 325 | 326 | Tips: ANY service using TCP is shootback-able. HTTP/FTP/Proxy/SSH/VNC/... 327 | """, 328 | formatter_class=argparse.RawDescriptionHelpFormatter 329 | ) 330 | parser.add_argument("-m", "--master", required=True, 331 | metavar="host:port", 332 | help="master address, usually an Public-IP. eg: 2.3.3.3:5500") 333 | parser.add_argument("-t", "--target", required=True, 334 | metavar="host:port", 335 | help="where the traffic from master should be tunneled to, usually not public. eg: 10.1.2.3:80") 336 | parser.add_argument("-k", "--secretkey", default="shootback", 337 | help="secretkey to identity master and slaver, should be set to the same value in both side") 338 | parser.add_argument("-v", "--verbose", action="count", default=0, 339 | help="verbose output") 340 | parser.add_argument("-q", "--quiet", action="count", default=0, 341 | help="quiet output, only display warning and errors, use two to disable output") 342 | parser.add_argument("-V", "--version", action="version", version="shootback {}-slaver".format(version_info())) 343 | parser.add_argument("--ttl", default=300, type=int, dest="SPARE_SLAVER_TTL", 344 | help="standing-by slaver's TTL, default is 300. " 345 | "this value is optimized for most cases") 346 | parser.add_argument("--max-standby", default=5, type=int, dest="max_spare_count", 347 | help="max standby slaver TCP connections count, default is 5. " 348 | "which is enough for more than 800 concurrency. " 349 | "while working connections are always unlimited") 350 | parser.add_argument('--ssl', action='store_true', help='[experimental] try using ssl for data encryption. ' 351 | 'It may be enabled by default in future version') 352 | 353 | return parser.parse_args() 354 | 355 | 356 | def main_slaver(): 357 | global SPARE_SLAVER_TTL 358 | 359 | args = argparse_slaver() 360 | 361 | if args.verbose and args.quiet: 362 | print("-v and -q should not appear together") 363 | exit(1) 364 | 365 | communicate_addr = split_host(args.master) 366 | target_addr = split_host(args.target) 367 | 368 | set_secretkey(args.secretkey) 369 | 370 | SPARE_SLAVER_TTL = args.SPARE_SLAVER_TTL 371 | max_spare_count = args.max_spare_count 372 | if args.quiet < 2: 373 | if args.verbose: 374 | level = logging.DEBUG 375 | elif args.quiet: 376 | level = logging.WARNING 377 | else: 378 | level = logging.INFO 379 | configure_logging(level) 380 | 381 | log.info("shootback {} slaver running".format(version_info())) 382 | log.info("author: {} site: {}".format(__author__, __website__)) 383 | log.info("Master: {}".format(fmt_addr(communicate_addr))) 384 | log.info("Target: {}".format(fmt_addr(target_addr))) 385 | 386 | # communicate_addr = ("localhost", 12345) 387 | # target_addr = ("93.184.216.34", 80) # www.example.com 388 | 389 | run_slaver(communicate_addr, target_addr, 390 | max_spare_count=max_spare_count, 391 | ssl=args.ssl, 392 | ) 393 | 394 | 395 | if __name__ == '__main__': 396 | main_slaver() 397 | --------------------------------------------------------------------------------