pax_global_header00006660000000000000000000000064115710707100014511gustar00rootroot0000000000000052 comment=a8b71f6387bda654c68e0320d03a0e9bb96a47f7 apenwarr-sshuttle-fb849f7/000077500000000000000000000000001157107071000156345ustar00rootroot00000000000000apenwarr-sshuttle-fb849f7/.gitignore000066400000000000000000000000561157107071000176250ustar00rootroot00000000000000*.pyc *~ *.8 /.do_built /.do_built.dir /.redo apenwarr-sshuttle-fb849f7/LICENSE000066400000000000000000000612611157107071000166470ustar00rootroot00000000000000 GNU LIBRARY GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1991 Free Software Foundation, Inc. 675 Mass Ave, Cambridge, MA 02139, USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the library GPL. It is numbered 2 because it goes with version 2 of the ordinary GPL.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Library General Public License, applies to some specially designated Free Software Foundation software, and to any other libraries whose authors decide to use it. You can use it for your libraries, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library, or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link a program with the library, you must provide complete object files to the recipients so that they can relink them with the library, after making changes to the library and recompiling it. And you must show them these terms so they know their rights. Our method of protecting your rights has two steps: (1) copyright the library, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the library. Also, for each distributor's protection, we want to make certain that everyone understands that there is no warranty for this free library. If the library is modified by someone else and passed on, we want its recipients to know that what they have is not the original version, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that companies distributing free software will individually obtain patent licenses, thus in effect transforming the program into proprietary software. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License, which was designed for utility programs. This license, the GNU Library General Public License, applies to certain designated libraries. This license is quite different from the ordinary one; be sure to read it in full, and don't assume that anything in it is the same as in the ordinary license. The reason we have a separate public license for some libraries is that they blur the distinction we usually make between modifying or adding to a program and simply using it. Linking a program with a library, without changing the library, is in some sense simply using the library, and is analogous to running a utility program or application program. However, in a textual and legal sense, the linked executable is a combined work, a derivative of the original library, and the ordinary General Public License treats it as such. Because of this blurred distinction, using the ordinary General Public License for libraries did not effectively promote software sharing, because most developers did not use the libraries. We concluded that weaker conditions might promote sharing better. However, unrestricted linking of non-free programs would deprive the users of those programs of all benefit from the free status of the libraries themselves. This Library General Public License is intended to permit developers of non-free programs to use free libraries, while preserving your freedom as a user of such programs to change the free libraries that are incorporated in them. (We have not seen how to achieve this as regards changes in header files, but we have achieved it as regards changes in the actual functions of the Library.) The hope is that this will lead to faster development of free libraries. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, while the latter only works together with the library. Note that it is possible for a library to be covered by the ordinary General Public License rather than by this special one. GNU LIBRARY GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Library General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also compile or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. c) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. d) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Library General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS Appendix: How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! apenwarr-sshuttle-fb849f7/Makefile000066400000000000000000000000671157107071000172770ustar00rootroot00000000000000all: Makefile: @ %: FORCE +./do $@ .PHONY: FORCE apenwarr-sshuttle-fb849f7/README.md000066400000000000000000000177371157107071000171320ustar00rootroot00000000000000 WARNING: On MacOS 10.6 (at least up to 10.6.6), your network will stop responding about 10 minutes after the first time you start sshuttle, because of a MacOS kernel bug relating to arp and the net.inet.ip.scopedroute sysctl. To fix it, just switch your wireless off and on. Sshuttle makes the kernel setting it changes permanent, so this won't happen again, even after a reboot. sshuttle: where transparent proxy meets VPN meets ssh ===================================================== As far as I know, sshuttle is the only program that solves the following common case: - Your client machine (or router) is Linux, FreeBSD, or MacOS. - You have access to a remote network via ssh. - You don't necessarily have admin access on the remote network. - The remote network has no VPN, or only stupid/complex VPN protocols (IPsec, PPTP, etc). Or maybe you are the admin and you just got frustrated with the awful state of VPN tools. - You don't want to create an ssh port forward for every single host/port on the remote network. - You hate openssh's port forwarding because it's randomly slow and/or stupid. - You can't use openssh's PermitTunnel feature because it's disabled by default on openssh servers; plus it does TCP-over-TCP, which has terrible performance (see below). Prerequisites ------------- - sudo, su, or logged in as root on your client machine. (The server doesn't need admin access.) - If you use Linux on your client machine: iptables installed on the client, including at least the iptables DNAT, REDIRECT, and ttl modules. These are installed by default on most Linux distributions. (The server doesn't need iptables and doesn't need to be Linux.) - If you use MacOS or BSD on your client machine: Your kernel needs to be compiled with IPFIREWALL_FORWARD (MacOS has this by default) and you need to have ipfw available. (The server doesn't need to be MacOS or BSD.) This is how you use it: ----------------------- - git clone git://github.com/apenwarr/sshuttle on your client machine. You'll need root or sudo access, and python needs to be installed. - The most basic use of sshuttle looks like: ./sshuttle -r username@sshserver 0.0.0.0/0 -vv - There is a shortcut for 0.0.0.0/0 for those that value their wrists ./sshuttle -r username@sshserver 0/0 -vv - If you would also like your DNS queries to be proxied through the DNS server of the server you are connect to: ./sshuttle --dns -vvr username@sshserver 0/0 The above is probably what you want to use to prevent local network attacks such as Firesheep and friends. (You may be prompted for one or more passwords; first, the local password to become root using either sudo or su, and then the remote ssh password. Or you might have sudo and ssh set up to not require passwords, in which case you won't be prompted at all.) That's it! Now your local machine can access the remote network as if you were right there. And if your "client" machine is a router, everyone on your local network can make connections to your remote network. You don't need to install sshuttle on the remote server; the remote server just needs to have python available. sshuttle will automatically upload and run its source code to the remote python interpreter. This creates a transparent proxy server on your local machine for all IP addresses that match 0.0.0.0/0. (You can use more specific IP addresses if you want; use any number of IP addresses or subnets to change which addresses get proxied. Using 0.0.0.0/0 proxies everything, which is interesting if you don't trust the people on your local network.) Any TCP session you initiate to one of the proxied IP addresses will be captured by sshuttle and sent over an ssh session to the remote copy of sshuttle, which will then regenerate the connection on that end, and funnel the data back and forth through ssh. Fun, right? A poor man's instant VPN, and you don't even have to have admin access on the server. Theory of Operation ------------------- sshuttle is not exactly a VPN, and not exactly port forwarding. It's kind of both, and kind of neither. It's like a VPN, since it can forward every port on an entire network, not just ports you specify. Conveniently, it lets you use the "real" IP addresses of each host rather than faking port numbers on localhost. On the other hand, the way it *works* is more like ssh port forwarding than a VPN. Normally, a VPN forwards your data one packet at a time, and doesn't care about individual connections; ie. it's "stateless" with respect to the traffic. sshuttle is the opposite of stateless; it tracks every single connection. You could compare sshuttle to something like the old Slirp program, which was a userspace TCP/IP implementation that did something similar. But it operated on a packet-by-packet basis on the client side, reassembling the packets on the server side. That worked okay back in the "real live serial port" days, because serial ports had predictable latency and buffering. But you can't safely just forward TCP packets over a TCP session (like ssh), because TCP's performance depends fundamentally on packet loss; it must experience packet loss in order to know when to slow down! At the same time, the outer TCP session (ssh, in this case) is a reliable transport, which means that what you forward through the tunnel never experiences packet loss. The ssh session itself experiences packet loss, of course, but TCP fixes it up and ssh (and thus you) never know the difference. But neither does your inner TCP session, and extremely screwy performance ensues. sshuttle assembles the TCP stream locally, multiplexes it statefully over an ssh session, and disassembles it back into packets at the other end. So it never ends up doing TCP-over-TCP. It's just data-over-TCP, which is safe. Useless Trivia -------------- Back in 1998 (12 years ago! Yikes!), I released the first version of Tunnel Vision, a semi-intelligent VPN client for Linux. Unfortunately, I made two big mistakes: I implemented the key exchange myself (oops), and I ended up doing TCP-over-TCP (double oops). The resulting program worked okay - and people used it for years - but the performance was always a bit funny. And nobody ever found any security flaws in my key exchange, either, but that doesn't mean anything. :) The same year, dcoombs and I also released Fast Forward, a proxy server supporting transparent proxying. Among other things, we used it for automatically splitting traffic across more than one Internet connection (a tool we called "Double Vision"). I was still in university at the time. A couple years after that, one of my professors was working with some graduate students on the technology that would eventually become Slipstream Internet Acceleration. He asked me to do a contract for him to build an initial prototype of a transparent proxy server for mobile networks. The idea was similar to sshuttle: if you reassemble and then disassemble the TCP packets, you can reduce latency and improve performance vs. just forwarding the packets over a plain VPN or mobile network. (It's unlikely that any of my code has persisted in the Slipstream product today, but the concept is still pretty cool. I'm still horrified that people use plain TCP on complex mobile networks with crazily variable latency, for which it was never really intended.) That project I did for Slipstream was what first gave me the idea to merge the concepts of Fast Forward, Double Vision, and Tunnel Vision into a single program that was the best of all worlds. And here we are, at last, 10 years later. You're welcome. -- Avery Pennarun Mailing list: Subscribe by sending a message to List archives are at: http://groups.google.com/group/sshuttle apenwarr-sshuttle-fb849f7/all.do000066400000000000000000000005041157107071000167270ustar00rootroot00000000000000exec >&2 UI= [ "$(uname)" = "Darwin" ] && UI=ui-macos/all redo-ifchange sshuttle.8 $UI echo echo "What now?" [ -z "$UI" ] || echo "- Try the MacOS GUI: open ui-macos/Sshuttle*.app" echo "- Run sshuttle: ./sshuttle --dns -r HOSTNAME 0/0" echo "- Read the README: less README.md" echo "- Read the man page: less sshuttle.md" apenwarr-sshuttle-fb849f7/assembler.py000066400000000000000000000013741157107071000201700ustar00rootroot00000000000000import sys, zlib z = zlib.decompressobj() mainmod = sys.modules[__name__] while 1: name = sys.stdin.readline().strip() if name: nbytes = int(sys.stdin.readline()) if verbosity >= 2: sys.stderr.write('server: assembling %r (%d bytes)\n' % (name, nbytes)) content = z.decompress(sys.stdin.read(nbytes)) exec compile(content, name, "exec") # FIXME: this crushes everything into a single module namespace, # then makes each of the module names point at this one. Gross. assert(name.endswith('.py')) modname = name[:-3] mainmod.__dict__[modname] = mainmod else: break verbose = verbosity sys.stderr.flush() sys.stdout.flush() main() apenwarr-sshuttle-fb849f7/clean.do000066400000000000000000000001141157107071000172360ustar00rootroot00000000000000redo ui-macos/clean rm -f *~ */*~ .*~ */.*~ *.8 *.tmp */*.tmp *.pyc */*.pyc apenwarr-sshuttle-fb849f7/client.py000066400000000000000000000304461157107071000174730ustar00rootroot00000000000000import struct, socket, select, errno, re, signal, time import compat.ssubprocess as ssubprocess import helpers, ssnet, ssh, ssyslog from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from helpers import * _extra_fd = os.open('/dev/null', os.O_RDONLY) def got_signal(signum, frame): log('exiting on signal %d\n' % signum) sys.exit(1) _pidname = None def check_daemon(pidfile): global _pidname _pidname = os.path.abspath(pidfile) try: oldpid = open(_pidname).read(1024) except IOError, e: if e.errno == errno.ENOENT: return # no pidfile, ok else: raise Fatal("can't read %s: %s" % (_pidname, e)) if not oldpid: os.unlink(_pidname) return # invalid pidfile, ok oldpid = int(oldpid.strip() or 0) if oldpid <= 0: os.unlink(_pidname) return # invalid pidfile, ok try: os.kill(oldpid, 0) except OSError, e: if e.errno == errno.ESRCH: os.unlink(_pidname) return # outdated pidfile, ok elif e.errno == errno.EPERM: pass else: raise raise Fatal("%s: sshuttle is already running (pid=%d)" % (_pidname, oldpid)) def daemonize(): if os.fork(): os._exit(0) os.setsid() if os.fork(): os._exit(0) outfd = os.open(_pidname, os.O_WRONLY|os.O_CREAT|os.O_EXCL, 0666) try: os.write(outfd, '%d\n' % os.getpid()) finally: os.close(outfd) os.chdir("/") # Normal exit when killed, or try/finally won't work and the pidfile won't # be deleted. signal.signal(signal.SIGTERM, got_signal) si = open('/dev/null', 'r+') os.dup2(si.fileno(), 0) os.dup2(si.fileno(), 1) si.close() ssyslog.stderr_to_syslog() def daemon_cleanup(): try: os.unlink(_pidname) except OSError, e: if e.errno == errno.ENOENT: pass else: raise def original_dst(sock): try: SO_ORIGINAL_DST = 80 SOCKADDR_MIN = 16 sockaddr_in = sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, SOCKADDR_MIN) (proto, port, a,b,c,d) = struct.unpack('!HHBBBB', sockaddr_in[:8]) assert(socket.htons(proto) == socket.AF_INET) ip = '%d.%d.%d.%d' % (a,b,c,d) return (ip,port) except socket.error, e: if e.args[0] == errno.ENOPROTOOPT: return sock.getsockname() raise class FirewallClient: def __init__(self, port, subnets_include, subnets_exclude, dnsport): self.port = port self.auto_nets = [] self.subnets_include = subnets_include self.subnets_exclude = subnets_exclude self.dnsport = dnsport argvbase = ([sys.argv[1], sys.argv[0], sys.argv[1]] + ['-v'] * (helpers.verbose or 0) + ['--firewall', str(port), str(dnsport)]) if ssyslog._p: argvbase += ['--syslog'] argv_tries = [ ['sudo', '-p', '[local sudo] Password: '] + argvbase, ['su', '-c', ' '.join(argvbase)], argvbase ] # we can't use stdin/stdout=subprocess.PIPE here, as we normally would, # because stupid Linux 'su' requires that stdin be attached to a tty. # Instead, attach a *bidirectional* socket to its stdout, and use # that for talking in both directions. (s1,s2) = socket.socketpair() def setup(): # run in the child process s2.close() e = None if os.getuid() == 0: argv_tries = argv_tries[-1:] # last entry only for argv in argv_tries: try: if argv[0] == 'su': sys.stderr.write('[local su] ') self.p = ssubprocess.Popen(argv, stdout=s1, preexec_fn=setup) e = None break except OSError, e: pass self.argv = argv s1.close() self.pfile = s2.makefile('wb+') if e: log('Spawning firewall manager: %r\n' % self.argv) raise Fatal(e) line = self.pfile.readline() self.check() if line != 'READY\n': raise Fatal('%r expected READY, got %r' % (self.argv, line)) def check(self): rv = self.p.poll() if rv: raise Fatal('%r returned %d' % (self.argv, rv)) def start(self): self.pfile.write('ROUTES\n') for (ip,width) in self.subnets_include+self.auto_nets: self.pfile.write('%d,0,%s\n' % (width, ip)) for (ip,width) in self.subnets_exclude: self.pfile.write('%d,1,%s\n' % (width, ip)) self.pfile.write('GO\n') self.pfile.flush() line = self.pfile.readline() self.check() if line != 'STARTED\n': raise Fatal('%r expected STARTED, got %r' % (self.argv, line)) def sethostip(self, hostname, ip): assert(not re.search(r'[^-\w]', hostname)) assert(not re.search(r'[^0-9.]', ip)) self.pfile.write('HOST %s,%s\n' % (hostname, ip)) self.pfile.flush() def done(self): self.pfile.close() rv = self.p.wait() if rv: raise Fatal('cleanup: %r returned %d' % (self.argv, rv)) def onaccept(listener, mux, handlers): global _extra_fd try: sock,srcip = listener.accept() except socket.error, e: if e.args[0] in [errno.EMFILE, errno.ENFILE]: debug1('Rejected incoming connection: too many open files!\n') # free up an fd so we can eat the connection os.close(_extra_fd) try: sock,srcip = listener.accept() sock.close() finally: _extra_fd = os.open('/dev/null', os.O_RDONLY) return else: raise dstip = original_dst(sock) debug1('Accept: %s:%r -> %s:%r.\n' % (srcip[0],srcip[1], dstip[0],dstip[1])) if dstip[1] == listener.getsockname()[1] and islocal(dstip[0]): debug1("-- ignored: that's my address!\n") sock.close() return chan = mux.next_channel() if not chan: log('warning: too many open channels. Discarded connection.\n') sock.close() return mux.send(chan, ssnet.CMD_CONNECT, '%s,%s' % dstip) outwrap = MuxWrapper(mux, chan) handlers.append(Proxy(SockWrapper(sock, sock), outwrap)) dnsreqs = {} def dns_done(chan, data): peer,sock,timeout = dnsreqs.get(chan) or (None,None,None) debug3('dns_done: channel=%r peer=%r\n' % (chan, peer)) if peer: del dnsreqs[chan] debug3('doing sendto %r\n' % (peer,)) sock.sendto(data, peer) def ondns(listener, mux, handlers): pkt,peer = listener.recvfrom(4096) now = time.time() if pkt: debug1('DNS request from %r: %d bytes\n' % (peer, len(pkt))) chan = mux.next_channel() dnsreqs[chan] = peer,listener,now+30 mux.send(chan, ssnet.CMD_DNS_REQ, pkt) mux.channels[chan] = lambda cmd,data: dns_done(chan,data) for chan,(peer,sock,timeout) in dnsreqs.items(): if timeout < now: del dnsreqs[chan] debug3('Remaining DNS requests: %d\n' % len(dnsreqs)) def _main(listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, seed_hosts, auto_nets, syslog, daemon): handlers = [] if helpers.verbose >= 1: helpers.logprefix = 'c : ' else: helpers.logprefix = 'client: ' debug1('connecting to server...\n') try: (serverproc, serversock) = ssh.connect(ssh_cmd, remotename, python, stderr=ssyslog._p and ssyslog._p.stdin, options=dict(latency_control=latency_control)) except socket.error, e: if e.args[0] == errno.EPIPE: raise Fatal("failed to establish ssh session (1)") else: raise mux = Mux(serversock, serversock) handlers.append(mux) expected = 'SSHUTTLE0001' try: v = 'x' while v and v != '\0': v = serversock.recv(1) v = 'x' while v and v != '\0': v = serversock.recv(1) initstring = serversock.recv(len(expected)) except socket.error, e: if e.args[0] == errno.ECONNRESET: raise Fatal("failed to establish ssh session (2)") else: raise rv = serverproc.poll() if rv: raise Fatal('server died with error code %d' % rv) if initstring != expected: raise Fatal('expected server init string %r; got %r' % (expected, initstring)) debug1('connected.\n') print 'Connected.' sys.stdout.flush() if daemon: daemonize() log('daemonizing (%s).\n' % _pidname) elif syslog: debug1('switching to syslog.\n') ssyslog.stderr_to_syslog() def onroutes(routestr): if auto_nets: for line in routestr.strip().split('\n'): (ip,width) = line.split(',', 1) fw.auto_nets.append((ip,int(width))) # we definitely want to do this *after* starting ssh, or we might end # up intercepting the ssh connection! # # Moreover, now that we have the --auto-nets option, we have to wait # for the server to send us that message anyway. Even if we haven't # set --auto-nets, we might as well wait for the message first, then # ignore its contents. mux.got_routes = None fw.start() mux.got_routes = onroutes def onhostlist(hostlist): debug2('got host list: %r\n' % hostlist) for line in hostlist.strip().split(): if line: name,ip = line.split(',', 1) fw.sethostip(name, ip) mux.got_host_list = onhostlist handlers.append(Handler([listener], lambda: onaccept(listener, mux, handlers))) if dnslistener: handlers.append(Handler([dnslistener], lambda: ondns(dnslistener, mux, handlers))) if seed_hosts != None: debug1('seed_hosts: %r\n' % seed_hosts) mux.send(0, ssnet.CMD_HOST_REQ, '\n'.join(seed_hosts)) while 1: rv = serverproc.poll() if rv: raise Fatal('server died with error code %d' % rv) ssnet.runonce(handlers, mux) if latency_control: mux.check_fullness() mux.callback() def main(listenip, ssh_cmd, remotename, python, latency_control, dns, seed_hosts, auto_nets, subnets_include, subnets_exclude, syslog, daemon, pidfile): if syslog: ssyslog.start_syslog() if daemon: try: check_daemon(pidfile) except Fatal, e: log("%s\n" % e) return 5 debug1('Starting sshuttle proxy.\n') if listenip[1]: ports = [listenip[1]] else: ports = xrange(12300,9000,-1) last_e = None bound = False debug2('Binding:') for port in ports: debug2(' %d' % port) listener = socket.socket() listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) dnslistener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) dnslistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: listener.bind((listenip[0], port)) dnslistener.bind((listenip[0], port)) bound = True break except socket.error, e: last_e = e debug2('\n') if not bound: assert(last_e) raise last_e listener.listen(10) listenip = listener.getsockname() debug1('Listening on %r.\n' % (listenip,)) if dns: dnsip = dnslistener.getsockname() debug1('DNS listening on %r.\n' % (dnsip,)) dnsport = dnsip[1] else: dnsport = 0 dnslistener = None fw = FirewallClient(listenip[1], subnets_include, subnets_exclude, dnsport) try: return _main(listener, fw, ssh_cmd, remotename, python, latency_control, dnslistener, seed_hosts, auto_nets, syslog, daemon) finally: try: if daemon: # it's not our child anymore; can't waitpid fw.p.returncode = 0 fw.done() finally: if daemon: daemon_cleanup() apenwarr-sshuttle-fb849f7/compat/000077500000000000000000000000001157107071000171175ustar00rootroot00000000000000apenwarr-sshuttle-fb849f7/compat/__init__.py000066400000000000000000000000001157107071000212160ustar00rootroot00000000000000apenwarr-sshuttle-fb849f7/compat/ssubprocess.py000066400000000000000000001312011157107071000220420ustar00rootroot00000000000000# subprocess - Subprocesses with accessible I/O streams # # For more information about this module, see PEP 324. # # This module should remain compatible with Python 2.2, see PEP 291. # # Copyright (c) 2003-2005 by Peter Astrand # # Licensed to PSF under a Contributor Agreement. # See http://www.python.org/2.4/license for licensing details. r"""subprocess - Subprocesses with accessible I/O streams This module allows you to spawn processes, connect to their input/output/error pipes, and obtain their return codes. This module intends to replace several other, older modules and functions, like: os.system os.spawn* os.popen* popen2.* commands.* Information about how the subprocess module can be used to replace these modules and functions can be found below. Using the subprocess module =========================== This module defines one class called Popen: class Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0): Arguments are: args should be a string, or a sequence of program arguments. The program to execute is normally the first item in the args sequence or string, but can be explicitly set by using the executable argument. On UNIX, with shell=False (default): In this case, the Popen class uses os.execvp() to execute the child program. args should normally be a sequence. A string will be treated as a sequence with the string as the only item (the program to execute). On UNIX, with shell=True: If args is a string, it specifies the command string to execute through the shell. If args is a sequence, the first item specifies the command string, and any additional items will be treated as additional shell arguments. On Windows: the Popen class uses CreateProcess() to execute the child program, which operates on strings. If args is a sequence, it will be converted to a string using the list2cmdline method. Please note that not all MS Windows applications interpret the command line the same way: The list2cmdline is designed for applications using the same rules as the MS C runtime. bufsize, if given, has the same meaning as the corresponding argument to the built-in open() function: 0 means unbuffered, 1 means line buffered, any other positive value means use a buffer of (approximately) that size. A negative bufsize means to use the system default, which usually means fully buffered. The default value for bufsize is 0 (unbuffered). stdin, stdout and stderr specify the executed programs' standard input, standard output and standard error file handles, respectively. Valid values are PIPE, an existing file descriptor (a positive integer), an existing file object, and None. PIPE indicates that a new pipe to the child should be created. With None, no redirection will occur; the child's file handles will be inherited from the parent. Additionally, stderr can be STDOUT, which indicates that the stderr data from the applications should be captured into the same file handle as for stdout. If preexec_fn is set to a callable object, this object will be called in the child process just before the child is executed. If close_fds is true, all file descriptors except 0, 1 and 2 will be closed before the child process is executed. if shell is true, the specified command will be executed through the shell. If cwd is not None, the current directory will be changed to cwd before the child is executed. If env is not None, it defines the environment variables for the new process. If universal_newlines is true, the file objects stdout and stderr are opened as a text files, but lines may be terminated by any of '\n', the Unix end-of-line convention, '\r', the Macintosh convention or '\r\n', the Windows convention. All of these external representations are seen as '\n' by the Python program. Note: This feature is only available if Python is built with universal newline support (the default). Also, the newlines attribute of the file objects stdout, stdin and stderr are not updated by the communicate() method. The startupinfo and creationflags, if given, will be passed to the underlying CreateProcess() function. They can specify things such as appearance of the main window and priority for the new process. (Windows only) This module also defines two shortcut functions: call(*popenargs, **kwargs): Run command with arguments. Wait for command to complete, then return the returncode attribute. The arguments are the same as for the Popen constructor. Example: retcode = call(["ls", "-l"]) check_call(*popenargs, **kwargs): Run command with arguments. Wait for command to complete. If the exit code was zero then return, otherwise raise CalledProcessError. The CalledProcessError object will have the return code in the returncode attribute. The arguments are the same as for the Popen constructor. Example: check_call(["ls", "-l"]) Exceptions ---------- Exceptions raised in the child process, before the new program has started to execute, will be re-raised in the parent. Additionally, the exception object will have one extra attribute called 'child_traceback', which is a string containing traceback information from the childs point of view. The most common exception raised is OSError. This occurs, for example, when trying to execute a non-existent file. Applications should prepare for OSErrors. A ValueError will be raised if Popen is called with invalid arguments. check_call() will raise CalledProcessError, if the called process returns a non-zero return code. Security -------- Unlike some other popen functions, this implementation will never call /bin/sh implicitly. This means that all characters, including shell metacharacters, can safely be passed to child processes. Popen objects ============= Instances of the Popen class have the following methods: poll() Check if child process has terminated. Returns returncode attribute. wait() Wait for child process to terminate. Returns returncode attribute. communicate(input=None) Interact with process: Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. Wait for process to terminate. The optional input argument should be a string to be sent to the child process, or None, if no data should be sent to the child. communicate() returns a tuple (stdout, stderr). Note: The data read is buffered in memory, so do not use this method if the data size is large or unlimited. The following attributes are also available: stdin If the stdin argument is PIPE, this attribute is a file object that provides input to the child process. Otherwise, it is None. stdout If the stdout argument is PIPE, this attribute is a file object that provides output from the child process. Otherwise, it is None. stderr If the stderr argument is PIPE, this attribute is file object that provides error output from the child process. Otherwise, it is None. pid The process ID of the child process. returncode The child return code. A None value indicates that the process hasn't terminated yet. A negative value -N indicates that the child was terminated by signal N (UNIX only). Replacing older functions with the subprocess module ==================================================== In this section, "a ==> b" means that b can be used as a replacement for a. Note: All functions in this section fail (more or less) silently if the executed program cannot be found; this module raises an OSError exception. In the following examples, we assume that the subprocess module is imported with "from subprocess import *". Replacing /bin/sh shell backquote --------------------------------- output=`mycmd myarg` ==> output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0] Replacing shell pipe line ------------------------- output=`dmesg | grep hda` ==> p1 = Popen(["dmesg"], stdout=PIPE) p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE) output = p2.communicate()[0] Replacing os.system() --------------------- sts = os.system("mycmd" + " myarg") ==> p = Popen("mycmd" + " myarg", shell=True) pid, sts = os.waitpid(p.pid, 0) Note: * Calling the program through the shell is usually not required. * It's easier to look at the returncode attribute than the exitstatus. A more real-world example would look like this: try: retcode = call("mycmd" + " myarg", shell=True) if retcode < 0: print >>sys.stderr, "Child was terminated by signal", -retcode else: print >>sys.stderr, "Child returned", retcode except OSError, e: print >>sys.stderr, "Execution failed:", e Replacing os.spawn* ------------------- P_NOWAIT example: pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg") ==> pid = Popen(["/bin/mycmd", "myarg"]).pid P_WAIT example: retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg") ==> retcode = call(["/bin/mycmd", "myarg"]) Vector example: os.spawnvp(os.P_NOWAIT, path, args) ==> Popen([path] + args[1:]) Environment example: os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env) ==> Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"}) Replacing os.popen* ------------------- pipe = os.popen(cmd, mode='r', bufsize) ==> pipe = Popen(cmd, shell=True, bufsize=bufsize, stdout=PIPE).stdout pipe = os.popen(cmd, mode='w', bufsize) ==> pipe = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE).stdin (child_stdin, child_stdout) = os.popen2(cmd, mode, bufsize) ==> p = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE, stdout=PIPE, close_fds=True) (child_stdin, child_stdout) = (p.stdin, p.stdout) (child_stdin, child_stdout, child_stderr) = os.popen3(cmd, mode, bufsize) ==> p = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) (child_stdin, child_stdout, child_stderr) = (p.stdin, p.stdout, p.stderr) (child_stdin, child_stdout_and_stderr) = os.popen4(cmd, mode, bufsize) ==> p = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True) (child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout) Replacing popen2.* ------------------ Note: If the cmd argument to popen2 functions is a string, the command is executed through /bin/sh. If it is a list, the command is directly executed. (child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode) ==> p = Popen(["somestring"], shell=True, bufsize=bufsize stdin=PIPE, stdout=PIPE, close_fds=True) (child_stdout, child_stdin) = (p.stdout, p.stdin) (child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize, mode) ==> p = Popen(["mycmd", "myarg"], bufsize=bufsize, stdin=PIPE, stdout=PIPE, close_fds=True) (child_stdout, child_stdin) = (p.stdout, p.stdin) The popen2.Popen3 and popen2.Popen4 basically works as subprocess.Popen, except that: * subprocess.Popen raises an exception if the execution fails * the capturestderr argument is replaced with the stderr argument. * stdin=PIPE and stdout=PIPE must be specified. * popen2 closes all filedescriptors by default, but you have to specify close_fds=True with subprocess.Popen. """ import sys mswindows = (sys.platform == "win32") import os import types import traceback import gc import signal # Exception classes used by this module. class CalledProcessError(Exception): """This exception is raised when a process run by check_call() returns a non-zero exit status. The exit status will be stored in the returncode attribute.""" def __init__(self, returncode, cmd): self.returncode = returncode self.cmd = cmd def __str__(self): return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) if mswindows: import threading import msvcrt if 0: # <-- change this to use pywin32 instead of the _subprocess driver import pywintypes from win32api import GetStdHandle, STD_INPUT_HANDLE, \ STD_OUTPUT_HANDLE, STD_ERROR_HANDLE from win32api import GetCurrentProcess, DuplicateHandle, \ GetModuleFileName, GetVersion from win32con import DUPLICATE_SAME_ACCESS, SW_HIDE from win32pipe import CreatePipe from win32process import CreateProcess, STARTUPINFO, \ GetExitCodeProcess, STARTF_USESTDHANDLES, \ STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE from win32process import TerminateProcess from win32event import WaitForSingleObject, INFINITE, WAIT_OBJECT_0 else: from _subprocess import * class STARTUPINFO: dwFlags = 0 hStdInput = None hStdOutput = None hStdError = None wShowWindow = 0 class pywintypes: error = IOError else: import select import errno import fcntl import pickle __all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", "CalledProcessError"] try: MAXFD = os.sysconf("SC_OPEN_MAX") except: MAXFD = 256 # True/False does not exist on 2.2.0 #try: # False #except NameError: # False = 0 # True = 1 _active = [] def _cleanup(): for inst in _active[:]: if inst._internal_poll(_deadstate=sys.maxint) >= 0: try: _active.remove(inst) except ValueError: # This can happen if two threads create a new Popen instance. # It's harmless that it was already removed, so ignore. pass PIPE = -1 STDOUT = -2 def call(*popenargs, **kwargs): """Run command with arguments. Wait for command to complete, then return the returncode attribute. The arguments are the same as for the Popen constructor. Example: retcode = call(["ls", "-l"]) """ return Popen(*popenargs, **kwargs).wait() def check_call(*popenargs, **kwargs): """Run command with arguments. Wait for command to complete. If the exit code was zero then return, otherwise raise CalledProcessError. The CalledProcessError object will have the return code in the returncode attribute. The arguments are the same as for the Popen constructor. Example: check_call(["ls", "-l"]) """ retcode = call(*popenargs, **kwargs) cmd = kwargs.get("args") if cmd is None: cmd = popenargs[0] if retcode: raise CalledProcessError(retcode, cmd) return retcode def list2cmdline(seq): """ Translate a sequence of arguments into a command line string, using the same rules as the MS C runtime: 1) Arguments are delimited by white space, which is either a space or a tab. 2) A string surrounded by double quotation marks is interpreted as a single argument, regardless of white space or pipe characters contained within. A quoted string can be embedded in an argument. 3) A double quotation mark preceded by a backslash is interpreted as a literal double quotation mark. 4) Backslashes are interpreted literally, unless they immediately precede a double quotation mark. 5) If backslashes immediately precede a double quotation mark, every pair of backslashes is interpreted as a literal backslash. If the number of backslashes is odd, the last backslash escapes the next double quotation mark as described in rule 3. """ # See # http://msdn.microsoft.com/library/en-us/vccelng/htm/progs_12.asp result = [] needquote = False for arg in seq: bs_buf = [] # Add a space to separate this argument from the others if result: result.append(' ') needquote = (" " in arg) or ("\t" in arg) or ("|" in arg) or not arg if needquote: result.append('"') for c in arg: if c == '\\': # Don't know if we need to double yet. bs_buf.append(c) elif c == '"': # Double backslashes. result.append('\\' * len(bs_buf)*2) bs_buf = [] result.append('\\"') else: # Normal char if bs_buf: result.extend(bs_buf) bs_buf = [] result.append(c) # Add remaining backslashes, if any. if bs_buf: result.extend(bs_buf) if needquote: result.extend(bs_buf) result.append('"') return ''.join(result) def _closerange(start, max): try: os.closerange(start, max) except AttributeError: for i in xrange(start, max): try: os.close(i) except: pass class Popen(object): def __init__(self, args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0): """Create new Popen instance.""" _cleanup() self._child_created = False if not isinstance(bufsize, (int, long)): raise TypeError("bufsize must be an integer") if mswindows: if preexec_fn is not None: raise ValueError("preexec_fn is not supported on Windows " "platforms") if close_fds and (stdin is not None or stdout is not None or stderr is not None): raise ValueError("close_fds is not supported on Windows " "platforms if you redirect stdin/stdout/stderr") else: # POSIX if startupinfo is not None: raise ValueError("startupinfo is only supported on Windows " "platforms") if creationflags != 0: raise ValueError("creationflags is only supported on Windows " "platforms") self.stdin = None self.stdout = None self.stderr = None self.pid = None self.returncode = None self.universal_newlines = universal_newlines # Input and output objects. The general principle is like # this: # # Parent Child # ------ ----- # p2cwrite ---stdin---> p2cread # c2pread <--stdout--- c2pwrite # errread <--stderr--- errwrite # # On POSIX, the child objects are file descriptors. On # Windows, these are Windows file handles. The parent objects # are file descriptors on both platforms. The parent objects # are None when not using PIPEs. The child objects are None # when not redirecting. (p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) = self._get_handles(stdin, stdout, stderr) self._execute_child(args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) # On Windows, you cannot just redirect one or two handles: You # either have to redirect all three or none. If the subprocess # user has only redirected one or two handles, we are # automatically creating PIPEs for the rest. We should close # these after the process is started. See bug #1124861. if mswindows: if stdin is None and p2cwrite is not None: os.close(p2cwrite) p2cwrite = None if stdout is None and c2pread is not None: os.close(c2pread) c2pread = None if stderr is None and errread is not None: os.close(errread) errread = None if p2cwrite is not None: self.stdin = os.fdopen(p2cwrite, 'wb', bufsize) if c2pread is not None: if universal_newlines: self.stdout = os.fdopen(c2pread, 'rU', bufsize) else: self.stdout = os.fdopen(c2pread, 'rb', bufsize) if errread is not None: if universal_newlines: self.stderr = os.fdopen(errread, 'rU', bufsize) else: self.stderr = os.fdopen(errread, 'rb', bufsize) def _translate_newlines(self, data): data = data.replace("\r\n", "\n") data = data.replace("\r", "\n") return data def __del__(self, sys=sys): if not self._child_created: # We didn't get to successfully create a child process. return # In case the child hasn't been waited on, check if it's done. self._internal_poll(_deadstate=sys.maxint) if self.returncode is None and _active is not None: # Child is still running, keep us alive until we can wait on it. _active.append(self) def communicate(self, input=None): """Interact with process: Send data to stdin. Read data from stdout and stderr, until end-of-file is reached. Wait for process to terminate. The optional input argument should be a string to be sent to the child process, or None, if no data should be sent to the child. communicate() returns a tuple (stdout, stderr).""" # Optimization: If we are only using one pipe, or no pipe at # all, using select() or threads is unnecessary. if [self.stdin, self.stdout, self.stderr].count(None) >= 2: stdout = None stderr = None if self.stdin: if input: self.stdin.write(input) self.stdin.close() elif self.stdout: stdout = self.stdout.read() self.stdout.close() elif self.stderr: stderr = self.stderr.read() self.stderr.close() self.wait() return (stdout, stderr) return self._communicate(input) def poll(self): return self._internal_poll() if mswindows: # # Windows methods # def _get_handles(self, stdin, stdout, stderr): """Construct and return tupel with IO objects: p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite """ if stdin is None and stdout is None and stderr is None: return (None, None, None, None, None, None) p2cread, p2cwrite = None, None c2pread, c2pwrite = None, None errread, errwrite = None, None if stdin is None: p2cread = GetStdHandle(STD_INPUT_HANDLE) if p2cread is not None: pass elif stdin is None or stdin == PIPE: p2cread, p2cwrite = CreatePipe(None, 0) # Detach and turn into fd p2cwrite = p2cwrite.Detach() p2cwrite = msvcrt.open_osfhandle(p2cwrite, 0) elif isinstance(stdin, int): p2cread = msvcrt.get_osfhandle(stdin) else: # Assuming file-like object p2cread = msvcrt.get_osfhandle(stdin.fileno()) p2cread = self._make_inheritable(p2cread) if stdout is None: c2pwrite = GetStdHandle(STD_OUTPUT_HANDLE) if c2pwrite is not None: pass elif stdout is None or stdout == PIPE: c2pread, c2pwrite = CreatePipe(None, 0) # Detach and turn into fd c2pread = c2pread.Detach() c2pread = msvcrt.open_osfhandle(c2pread, 0) elif isinstance(stdout, int): c2pwrite = msvcrt.get_osfhandle(stdout) else: # Assuming file-like object c2pwrite = msvcrt.get_osfhandle(stdout.fileno()) c2pwrite = self._make_inheritable(c2pwrite) if stderr is None: errwrite = GetStdHandle(STD_ERROR_HANDLE) if errwrite is not None: pass elif stderr is None or stderr == PIPE: errread, errwrite = CreatePipe(None, 0) # Detach and turn into fd errread = errread.Detach() errread = msvcrt.open_osfhandle(errread, 0) elif stderr == STDOUT: errwrite = c2pwrite elif isinstance(stderr, int): errwrite = msvcrt.get_osfhandle(stderr) else: # Assuming file-like object errwrite = msvcrt.get_osfhandle(stderr.fileno()) errwrite = self._make_inheritable(errwrite) return (p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) def _make_inheritable(self, handle): """Return a duplicate of handle, which is inheritable""" return DuplicateHandle(GetCurrentProcess(), handle, GetCurrentProcess(), 0, 1, DUPLICATE_SAME_ACCESS) def _find_w9xpopen(self): """Find and return absolut path to w9xpopen.exe""" w9xpopen = os.path.join(os.path.dirname(GetModuleFileName(0)), "w9xpopen.exe") if not os.path.exists(w9xpopen): # Eeek - file-not-found - possibly an embedding # situation - see if we can locate it in sys.exec_prefix w9xpopen = os.path.join(os.path.dirname(sys.exec_prefix), "w9xpopen.exe") if not os.path.exists(w9xpopen): raise RuntimeError("Cannot locate w9xpopen.exe, which is " "needed for Popen to work with your " "shell or platform.") return w9xpopen def _execute_child(self, args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite): """Execute program (MS Windows version)""" if not isinstance(args, types.StringTypes): args = list2cmdline(args) # Process startup details if startupinfo is None: startupinfo = STARTUPINFO() if None not in (p2cread, c2pwrite, errwrite): startupinfo.dwFlags |= STARTF_USESTDHANDLES startupinfo.hStdInput = p2cread startupinfo.hStdOutput = c2pwrite startupinfo.hStdError = errwrite if shell: startupinfo.dwFlags |= STARTF_USESHOWWINDOW startupinfo.wShowWindow = SW_HIDE comspec = os.environ.get("COMSPEC", "cmd.exe") args = comspec + " /c " + args if (GetVersion() >= 0x80000000L or os.path.basename(comspec).lower() == "command.com"): # Win9x, or using command.com on NT. We need to # use the w9xpopen intermediate program. For more # information, see KB Q150956 # (http://web.archive.org/web/20011105084002/http://support.microsoft.com/support/kb/articles/Q150/9/56.asp) w9xpopen = self._find_w9xpopen() args = '"%s" %s' % (w9xpopen, args) # Not passing CREATE_NEW_CONSOLE has been known to # cause random failures on win9x. Specifically a # dialog: "Your program accessed mem currently in # use at xxx" and a hopeful warning about the # stability of your system. Cost is Ctrl+C wont # kill children. creationflags |= CREATE_NEW_CONSOLE # Start the process try: hp, ht, pid, tid = CreateProcess(executable, args, # no special security None, None, int(not close_fds), creationflags, env, cwd, startupinfo) except pywintypes.error, e: # Translate pywintypes.error to WindowsError, which is # a subclass of OSError. FIXME: We should really # translate errno using _sys_errlist (or simliar), but # how can this be done from Python? raise WindowsError(*e.args) # Retain the process handle, but close the thread handle self._child_created = True self._handle = hp self.pid = pid ht.Close() # Child is launched. Close the parent's copy of those pipe # handles that only the child should have open. You need # to make sure that no handles to the write end of the # output pipe are maintained in this process or else the # pipe will not close when the child process exits and the # ReadFile will hang. if p2cread is not None: p2cread.Close() if c2pwrite is not None: c2pwrite.Close() if errwrite is not None: errwrite.Close() def _internal_poll(self, _deadstate=None): """Check if child process has terminated. Returns returncode attribute.""" if self.returncode is None: if WaitForSingleObject(self._handle, 0) == WAIT_OBJECT_0: self.returncode = GetExitCodeProcess(self._handle) return self.returncode def wait(self): """Wait for child process to terminate. Returns returncode attribute.""" if self.returncode is None: obj = WaitForSingleObject(self._handle, INFINITE) self.returncode = GetExitCodeProcess(self._handle) return self.returncode def _readerthread(self, fh, buffer): buffer.append(fh.read()) def _communicate(self, input): stdout = None # Return stderr = None # Return if self.stdout: stdout = [] stdout_thread = threading.Thread(target=self._readerthread, args=(self.stdout, stdout)) stdout_thread.setDaemon(True) stdout_thread.start() if self.stderr: stderr = [] stderr_thread = threading.Thread(target=self._readerthread, args=(self.stderr, stderr)) stderr_thread.setDaemon(True) stderr_thread.start() if self.stdin: if input is not None: self.stdin.write(input) self.stdin.close() if self.stdout: stdout_thread.join() if self.stderr: stderr_thread.join() # All data exchanged. Translate lists into strings. if stdout is not None: stdout = stdout[0] if stderr is not None: stderr = stderr[0] # Translate newlines, if requested. We cannot let the file # object do the translation: It is based on stdio, which is # impossible to combine with select (unless forcing no # buffering). if self.universal_newlines and hasattr(file, 'newlines'): if stdout: stdout = self._translate_newlines(stdout) if stderr: stderr = self._translate_newlines(stderr) self.wait() return (stdout, stderr) def send_signal(self, sig): """Send a signal to the process """ if sig == signal.SIGTERM: self.terminate() else: raise ValueError("Only SIGTERM is supported on Windows") def terminate(self): """Terminates the process """ TerminateProcess(self._handle, 1) kill = terminate else: # # POSIX methods # def _get_handles(self, stdin, stdout, stderr): """Construct and return tupel with IO objects: p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite """ p2cread, p2cwrite = None, None c2pread, c2pwrite = None, None errread, errwrite = None, None if stdin is None: pass elif stdin == PIPE: p2cread, p2cwrite = os.pipe() elif isinstance(stdin, int): p2cread = stdin else: # Assuming file-like object p2cread = stdin.fileno() if stdout is None: pass elif stdout == PIPE: c2pread, c2pwrite = os.pipe() elif isinstance(stdout, int): c2pwrite = stdout else: # Assuming file-like object c2pwrite = stdout.fileno() if stderr is None: pass elif stderr == PIPE: errread, errwrite = os.pipe() elif stderr == STDOUT: errwrite = c2pwrite elif isinstance(stderr, int): errwrite = stderr else: # Assuming file-like object errwrite = stderr.fileno() return (p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite) def _set_cloexec_flag(self, fd): try: cloexec_flag = fcntl.FD_CLOEXEC except AttributeError: cloexec_flag = 1 old = fcntl.fcntl(fd, fcntl.F_GETFD) fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag) def _close_fds(self, but): _closerange(3, but) _closerange(but + 1, MAXFD) def _execute_child(self, args, executable, preexec_fn, close_fds, cwd, env, universal_newlines, startupinfo, creationflags, shell, p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite): """Execute program (POSIX version)""" if isinstance(args, types.StringTypes): args = [args] else: args = list(args) if shell: args = ["/bin/sh", "-c"] + args if executable is None: executable = args[0] # For transferring possible exec failure from child to parent # The first char specifies the exception type: 0 means # OSError, 1 means some other error. errpipe_read, errpipe_write = os.pipe() self._set_cloexec_flag(errpipe_write) gc_was_enabled = gc.isenabled() # Disable gc to avoid bug where gc -> file_dealloc -> # write to stderr -> hang. http://bugs.python.org/issue1336 gc.disable() try: self.pid = os.fork() except: if gc_was_enabled: gc.enable() raise self._child_created = True if self.pid == 0: # Child try: # Close parent's pipe ends if p2cwrite is not None: os.close(p2cwrite) if c2pread is not None: os.close(c2pread) if errread is not None: os.close(errread) os.close(errpipe_read) # Dup fds for child if p2cread is not None: os.dup2(p2cread, 0) if c2pwrite is not None: os.dup2(c2pwrite, 1) if errwrite is not None: os.dup2(errwrite, 2) # Close pipe fds. Make sure we don't close the same # fd more than once, or standard fds. if p2cread is not None and p2cread not in (0,): os.close(p2cread) if c2pwrite is not None and c2pwrite not in (p2cread, 1): os.close(c2pwrite) if errwrite is not None and errwrite not in (p2cread, c2pwrite, 2): os.close(errwrite) # Close all other fds, if asked for if close_fds: self._close_fds(but=errpipe_write) if cwd is not None: os.chdir(cwd) if preexec_fn: preexec_fn() if env is None: os.execvp(executable, args) else: os.execvpe(executable, args, env) except: exc_type, exc_value, tb = sys.exc_info() # Save the traceback and attach it to the exception object exc_lines = traceback.format_exception(exc_type, exc_value, tb) exc_value.child_traceback = ''.join(exc_lines) os.write(errpipe_write, pickle.dumps(exc_value)) # This exitcode won't be reported to applications, so it # really doesn't matter what we return. os._exit(255) # Parent if gc_was_enabled: gc.enable() os.close(errpipe_write) if p2cread is not None and p2cwrite is not None: os.close(p2cread) if c2pwrite is not None and c2pread is not None: os.close(c2pwrite) if errwrite is not None and errread is not None: os.close(errwrite) # Wait for exec to fail or succeed; possibly raising exception data = os.read(errpipe_read, 1048576) # Exceptions limited to 1 MB os.close(errpipe_read) if data != "": os.waitpid(self.pid, 0) child_exception = pickle.loads(data) raise child_exception def _handle_exitstatus(self, sts): if os.WIFSIGNALED(sts): self.returncode = -os.WTERMSIG(sts) elif os.WIFEXITED(sts): self.returncode = os.WEXITSTATUS(sts) else: # Should never happen raise RuntimeError("Unknown child exit status!") def _internal_poll(self, _deadstate=None): """Check if child process has terminated. Returns returncode attribute.""" if self.returncode is None: try: pid, sts = os.waitpid(self.pid, os.WNOHANG) if pid == self.pid: self._handle_exitstatus(sts) except os.error: if _deadstate is not None: self.returncode = _deadstate return self.returncode def wait(self): """Wait for child process to terminate. Returns returncode attribute.""" if self.returncode is None: pid, sts = os.waitpid(self.pid, 0) self._handle_exitstatus(sts) return self.returncode def _communicate(self, input): read_set = [] write_set = [] stdout = None # Return stderr = None # Return if self.stdin: # Flush stdio buffer. This might block, if the user has # been writing to .stdin in an uncontrolled fashion. self.stdin.flush() if input: write_set.append(self.stdin) else: self.stdin.close() if self.stdout: read_set.append(self.stdout) stdout = [] if self.stderr: read_set.append(self.stderr) stderr = [] input_offset = 0 while read_set or write_set: try: rlist, wlist, xlist = select.select(read_set, write_set, []) except select.error, e: if e.args[0] == errno.EINTR: continue raise if self.stdin in wlist: # When select has indicated that the file is writable, # we can write up to PIPE_BUF bytes without risk # blocking. POSIX defines PIPE_BUF >= 512 chunk = input[input_offset : input_offset + 512] bytes_written = os.write(self.stdin.fileno(), chunk) input_offset += bytes_written if input_offset >= len(input): self.stdin.close() write_set.remove(self.stdin) if self.stdout in rlist: data = os.read(self.stdout.fileno(), 1024) if data == "": self.stdout.close() read_set.remove(self.stdout) stdout.append(data) if self.stderr in rlist: data = os.read(self.stderr.fileno(), 1024) if data == "": self.stderr.close() read_set.remove(self.stderr) stderr.append(data) # All data exchanged. Translate lists into strings. if stdout is not None: stdout = ''.join(stdout) if stderr is not None: stderr = ''.join(stderr) # Translate newlines, if requested. We cannot let the file # object do the translation: It is based on stdio, which is # impossible to combine with select (unless forcing no # buffering). if self.universal_newlines and hasattr(file, 'newlines'): if stdout: stdout = self._translate_newlines(stdout) if stderr: stderr = self._translate_newlines(stderr) self.wait() return (stdout, stderr) def send_signal(self, sig): """Send a signal to the process """ os.kill(self.pid, sig) def terminate(self): """Terminate the process with SIGTERM """ self.send_signal(signal.SIGTERM) def kill(self): """Kill the process with SIGKILL """ self.send_signal(signal.SIGKILL) def _demo_posix(): # # Example 1: Simple redirection: Get process list # plist = Popen(["ps"], stdout=PIPE).communicate()[0] print "Process list:" print plist # # Example 2: Change uid before executing child # if os.getuid() == 0: p = Popen(["id"], preexec_fn=lambda: os.setuid(100)) p.wait() # # Example 3: Connecting several subprocesses # print "Looking for 'hda'..." p1 = Popen(["dmesg"], stdout=PIPE) p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE) print repr(p2.communicate()[0]) # # Example 4: Catch execution error # print print "Trying a weird file..." try: print Popen(["/this/path/does/not/exist"]).communicate() except OSError, e: if e.errno == errno.ENOENT: print "The file didn't exist. I thought so..." print "Child traceback:" print e.child_traceback else: print "Error", e.errno else: print >>sys.stderr, "Gosh. No error." def _demo_windows(): # # Example 1: Connecting several subprocesses # print "Looking for 'PROMPT' in set output..." p1 = Popen("set", stdout=PIPE, shell=True) p2 = Popen('find "PROMPT"', stdin=p1.stdout, stdout=PIPE) print repr(p2.communicate()[0]) # # Example 2: Simple execution of program # print "Executing calc..." p = Popen("calc") p.wait() if 0 and __name__ == "__main__": if mswindows: _demo_windows() else: _demo_posix() apenwarr-sshuttle-fb849f7/default.8.do000066400000000000000000000002571157107071000177560ustar00rootroot00000000000000exec >&2 if pandoc /dev/null; then pandoc -s -r markdown -w man -o $3 $1.md else echo "Warning: pandoc not installed; can't generate manpages." redo-always fi apenwarr-sshuttle-fb849f7/do000077500000000000000000000070621157107071000161710ustar00rootroot00000000000000#!/bin/sh # # A minimal alternative to djb redo that doesn't support incremental builds. # For the full version, visit http://github.com/apenwarr/redo # # The author disclaims copyright to this source file and hereby places it in # the public domain. (2010 12 14) # # By default, no output coloring. green="" bold="" plain="" if [ -n "$TERM" -a "$TERM" != "dumb" ] && tty <&2 >/dev/null 2>&1; then green="$(printf '\033[32m')" bold="$(printf '\033[1m')" plain="$(printf '\033[m')" fi _dirsplit() { base=${1##*/} dir=${1%$base} } dirname() ( _dirsplit "$1" dir=${dir%/} echo "${dir:-.}" ) _dirsplit "$0" export REDO=$(cd "${dir:-.}" && echo "$PWD/$base") DO_TOP= if [ -z "$DO_BUILT" ]; then DO_TOP=1 [ -n "$*" ] || set all # only toplevel redo has a default target export DO_BUILT=$PWD/.do_built : >>"$DO_BUILT" echo "Removing previously built files..." >&2 sort -u "$DO_BUILT" | tee "$DO_BUILT.new" | while read f; do printf "%s\0%s.did\0" "$f" "$f"; done | xargs -0 rm -f 2>/dev/null mv "$DO_BUILT.new" "$DO_BUILT" DO_PATH=$DO_BUILT.dir export PATH=$DO_PATH:$PATH rm -rf "$DO_PATH" mkdir "$DO_PATH" for d in redo redo-ifchange; do ln -s "$REDO" "$DO_PATH/$d"; done [ -e /bin/true ] && TRUE=/bin/true || TRUE=/usr/bin/true for d in redo-ifcreate redo-stamp redo-always; do ln -s $TRUE "$DO_PATH/$d"; done fi _find_dofile_pwd() { dofile=default.$1.do while :; do dofile=default.${dofile#default.*.} [ -e "$dofile" -o "$dofile" = default.do ] && break done ext=${dofile#default} ext=${ext%.do} base=${1%$ext} } _find_dofile() { local prefix= while :; do _find_dofile_pwd "$1" [ -e "$dofile" ] && break [ "$PWD" = "/" ] && break target=${PWD##*/}/$target tmp=${PWD##*/}/$tmp prefix=${PWD##*/}/$prefix cd .. done base=$prefix$base } _run_dofile() { export DO_DEPTH="$DO_DEPTH " export REDO_TARGET=$PWD/$target local line1 set -e read line1 <"$PWD/$dofile" cmd=${line1#"#!/"} if [ "$cmd" != "$line1" ]; then /$cmd "$PWD/$dofile" "$@" >"$tmp.tmp2" else :; . "$PWD/$dofile" >"$tmp.tmp2" fi } _do() { local dir=$1 target=$2 tmp=$3 if [ ! -e "$target" ] || [ -d "$target" -a ! -e "$target.did" ]; then printf '%sdo %s%s%s%s\n' \ "$green" "$DO_DEPTH" "$bold" "$dir$target" "$plain" >&2 echo "$PWD/$target" >>"$DO_BUILT" dofile=$target.do base=$target ext= [ -e "$target.do" ] || _find_dofile "$target" if [ ! -e "$dofile" ]; then echo "do: $target: no .do file" >&2 return 1 fi [ ! -e "$DO_BUILT" ] || [ ! -d "$(dirname "$target")" ] || : >>"$target.did" ( _run_dofile "$base" "$ext" "$tmp.tmp" ) rv=$? if [ $rv != 0 ]; then printf "do: %s%s\n" "$DO_DEPTH" \ "$dir$target: got exit code $rv" >&2 rm -f "$tmp.tmp" "$tmp.tmp2" return $rv fi mv "$tmp.tmp" "$target" 2>/dev/null || ! test -s "$tmp.tmp2" || mv "$tmp.tmp2" "$target" 2>/dev/null rm -f "$tmp.tmp2" else echo "do $DO_DEPTH$target exists." >&2 fi } # Make corrections for directories that don't actually exist yet. _dir_shovel() { local dir base xdir=$1 xbase=$2 xbasetmp=$2 while [ ! -d "$xdir" -a -n "$xdir" ]; do _dirsplit "${xdir%/}" xbasetmp=${base}__$xbase xdir=$dir xbase=$base/$xbase echo "xbasetmp='$xbasetmp'" >&2 done } redo() { for i in "$@"; do _dirsplit "$i" _dir_shovel "$dir" "$base" dir=$xdir base=$xbase basetmp=$xbasetmp ( cd "$dir" && _do "$dir" "$base" "$basetmp" ) || return 1 done } set -e redo "$@" if [ -n "$DO_TOP" ]; then echo "Removing stamp files..." >&2 [ ! -e "$DO_BUILT" ] || while read f; do printf "%s.did\0" "$f"; done <"$DO_BUILT" | xargs -0 rm -f 2>/dev/null fi apenwarr-sshuttle-fb849f7/firewall.py000066400000000000000000000367101157107071000200220ustar00rootroot00000000000000import re, errno, socket, select, struct import compat.ssubprocess as ssubprocess import helpers, ssyslog from helpers import * # python doesn't have a definition for this IPPROTO_DIVERT = 254 def nonfatal(func, *args): try: func(*args) except Fatal, e: log('error: %s\n' % e) def ipt_chain_exists(name): argv = ['iptables', '-t', 'nat', '-nL'] p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) for line in p.stdout: if line.startswith('Chain %s ' % name): return True rv = p.wait() if rv: raise Fatal('%r returned %d' % (argv, rv)) def ipt(*args): argv = ['iptables', '-t', 'nat'] + list(args) debug1('>> %s\n' % ' '.join(argv)) rv = ssubprocess.call(argv) if rv: raise Fatal('%r returned %d' % (argv, rv)) _no_ttl_module = False def ipt_ttl(*args): global _no_ttl_module if not _no_ttl_module: # we avoid infinite loops by generating server-side connections # with ttl 42. This makes the client side not recapture those # connections, in case client == server. try: argsplus = list(args) + ['-m', 'ttl', '!', '--ttl', '42'] ipt(*argsplus) except Fatal: ipt(*args) # we only get here if the non-ttl attempt succeeds log('sshuttle: warning: your iptables is missing ' 'the ttl module.\n') _no_ttl_module = True else: ipt(*args) # We name the chain based on the transproxy port number so that it's possible # to run multiple copies of sshuttle at the same time. Of course, the # multiple copies shouldn't have overlapping subnets, or only the most- # recently-started one will win (because we use "-I OUTPUT 1" instead of # "-A OUTPUT"). def do_iptables(port, dnsport, subnets): chain = 'sshuttle-%s' % port # basic cleanup/setup of chains if ipt_chain_exists(chain): nonfatal(ipt, '-D', 'OUTPUT', '-j', chain) nonfatal(ipt, '-D', 'PREROUTING', '-j', chain) nonfatal(ipt, '-F', chain) ipt('-X', chain) if subnets or dnsport: ipt('-N', chain) ipt('-F', chain) ipt('-I', 'OUTPUT', '1', '-j', chain) ipt('-I', 'PREROUTING', '1', '-j', chain) if subnets: # create new subnet entries. Note that we're sorting in a very # particular order: we need to go from most-specific (largest swidth) # to least-specific, and at any given level of specificity, we want # excludes to come first. That's why the columns are in such a non- # intuitive order. for swidth,sexclude,snet in sorted(subnets, reverse=True): if sexclude: ipt('-A', chain, '-j', 'RETURN', '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp') else: ipt_ttl('-A', chain, '-j', 'REDIRECT', '--dest', '%s/%s' % (snet,swidth), '-p', 'tcp', '--to-ports', str(port)) if dnsport: nslist = resolvconf_nameservers() for ip in nslist: ipt_ttl('-A', chain, '-j', 'REDIRECT', '--dest', '%s/32' % ip, '-p', 'udp', '--dport', '53', '--to-ports', str(dnsport)) def ipfw_rule_exists(n): argv = ['ipfw', 'list'] p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) found = False for line in p.stdout: if line.startswith('%05d ' % n): if not ('ipttl 42' in line or ('skipto %d' % (n+1)) in line or 'check-state' in line): log('non-sshuttle ipfw rule: %r\n' % line.strip()) raise Fatal('non-sshuttle ipfw rule #%d already exists!' % n) found = True rv = p.wait() if rv: raise Fatal('%r returned %d' % (argv, rv)) return found _oldctls = {} def _fill_oldctls(prefix): argv = ['sysctl', prefix] p = ssubprocess.Popen(argv, stdout = ssubprocess.PIPE) for line in p.stdout: assert(line[-1] == '\n') (k,v) = line[:-1].split(': ', 1) _oldctls[k] = v rv = p.wait() if rv: raise Fatal('%r returned %d' % (argv, rv)) if not line: raise Fatal('%r returned no data' % (argv,)) def _sysctl_set(name, val): argv = ['sysctl', '-w', '%s=%s' % (name, val)] debug1('>> %s\n' % ' '.join(argv)) return ssubprocess.call(argv, stdout = open('/dev/null', 'w')) _changedctls = [] def sysctl_set(name, val, permanent=False): PREFIX = 'net.inet.ip' assert(name.startswith(PREFIX + '.')) val = str(val) if not _oldctls: _fill_oldctls(PREFIX) if not (name in _oldctls): debug1('>> No such sysctl: %r\n' % name) return False oldval = _oldctls[name] if val != oldval: rv = _sysctl_set(name, val) if rv==0 and permanent: debug1('>> ...saving permanently in /etc/sysctl.conf\n') f = open('/etc/sysctl.conf', 'a') f.write('\n' '# Added by sshuttle\n' '%s=%s\n' % (name, val)) f.close() else: _changedctls.append(name) return True def _udp_unpack(p): src = (socket.inet_ntoa(p[12:16]), struct.unpack('!H', p[20:22])[0]) dst = (socket.inet_ntoa(p[16:20]), struct.unpack('!H', p[22:24])[0]) return src, dst def _udp_repack(p, src, dst): addrs = socket.inet_aton(src[0]) + socket.inet_aton(dst[0]) ports = struct.pack('!HH', src[1], dst[1]) return p[:12] + addrs + ports + p[24:] _real_dns_server = [None] def _handle_diversion(divertsock, dnsport): p,tag = divertsock.recvfrom(4096) src,dst = _udp_unpack(p) debug3('got diverted packet from %r to %r\n' % (src, dst)) if dst[1] == 53: # outgoing DNS debug3('...packet is a DNS request.\n') _real_dns_server[0] = dst dst = ('127.0.0.1', dnsport) elif src[1] == dnsport: if islocal(src[0]): debug3('...packet is a DNS response.\n') src = _real_dns_server[0] else: log('weird?! unexpected divert from %r to %r\n' % (src, dst)) assert(0) newp = _udp_repack(p, src, dst) divertsock.sendto(newp, tag) def ipfw(*args): argv = ['ipfw', '-q'] + list(args) debug1('>> %s\n' % ' '.join(argv)) rv = ssubprocess.call(argv) if rv: raise Fatal('%r returned %d' % (argv, rv)) def do_ipfw(port, dnsport, subnets): sport = str(port) xsport = str(port+1) # cleanup any existing rules if ipfw_rule_exists(port): ipfw('delete', sport) while _changedctls: name = _changedctls.pop() oldval = _oldctls[name] _sysctl_set(name, oldval) if subnets or dnsport: sysctl_set('net.inet.ip.fw.enable', 1) changed = sysctl_set('net.inet.ip.scopedroute', 0, permanent=True) if changed: log("\n" " WARNING: ONE-TIME NETWORK DISRUPTION:\n" " =====================================\n" "sshuttle has changed a MacOS kernel setting to work around\n" "a bug in MacOS 10.6. This will cause your network to drop\n" "within 5-10 minutes unless you restart your network\n" "interface (change wireless networks or unplug/plug the\n" "ethernet port) NOW, then restart sshuttle. The fix is\n" "permanent; you only have to do this once.\n\n") sys.exit(1) ipfw('add', sport, 'check-state', 'ip', 'from', 'any', 'to', 'any') if subnets: # create new subnet entries for swidth,sexclude,snet in sorted(subnets, reverse=True): if sexclude: ipfw('add', sport, 'skipto', xsport, 'log', 'tcp', 'from', 'any', 'to', '%s/%s' % (snet,swidth)) else: ipfw('add', sport, 'fwd', '127.0.0.1,%d' % port, 'log', 'tcp', 'from', 'any', 'to', '%s/%s' % (snet,swidth), 'not', 'ipttl', '42', 'keep-state', 'setup') # This part is much crazier than it is on Linux, because MacOS (at least # 10.6, and probably other versions, and maybe FreeBSD too) doesn't # correctly fixup the dstip/dstport for UDP packets when it puts them # through a 'fwd' rule. It also doesn't fixup the srcip/srcport in the # response packet. In Linux iptables, all that happens magically for us, # so we just redirect the packets and relax. # # On MacOS, we have to fix the ports ourselves. For that, we use a # 'divert' socket, which receives raw packets and lets us mangle them. # # Here's how it works. Let's say the local DNS server is 1.1.1.1:53, # and the remote DNS server is 2.2.2.2:53, and the local transproxy port # is 10.0.0.1:12300, and a client machine is making a request from # 10.0.0.5:9999. We see a packet like this: # 10.0.0.5:9999 -> 1.1.1.1:53 # Since the destip:port matches one of our local nameservers, it will # match a 'fwd' rule, thus grabbing it on the local machine. However, # the local kernel will then see a packet addressed to *:53 and # not know what to do with it; there's nobody listening on port 53. Thus, # we divert it, rewriting it into this: # 10.0.0.5:9999 -> 10.0.0.1:12300 # This gets proxied out to the server, which sends it to 2.2.2.2:53, # and the answer comes back, and the proxy sends it back out like this: # 10.0.0.1:12300 -> 10.0.0.5:9999 # But that's wrong! The original machine expected an answer from # 1.1.1.1:53, so we have to divert the *answer* and rewrite it: # 1.1.1.1:53 -> 10.0.0.5:9999 # # See? Easy stuff. if dnsport: divertsock = socket.socket(socket.AF_INET, socket.SOCK_RAW, IPPROTO_DIVERT) divertsock.bind(('0.0.0.0', port)) # IP field is ignored nslist = resolvconf_nameservers() for ip in nslist: # relabel and then catch outgoing DNS requests ipfw('add', sport, 'divert', sport, 'log', 'udp', 'from', 'any', 'to', '%s/32' % ip, '53', 'not', 'ipttl', '42') # relabel DNS responses ipfw('add', sport, 'divert', sport, 'log', 'udp', 'from', 'any', str(dnsport), 'to', 'any', 'not', 'ipttl', '42') def do_wait(): while 1: r,w,x = select.select([sys.stdin, divertsock], [], []) if divertsock in r: _handle_diversion(divertsock, dnsport) if sys.stdin in r: return else: do_wait = None return do_wait def program_exists(name): paths = (os.getenv('PATH') or os.defpath).split(os.pathsep) for p in paths: fn = '%s/%s' % (p, name) if os.path.exists(fn): return not os.path.isdir(fn) and os.access(fn, os.X_OK) hostmap = {} def rewrite_etc_hosts(port): HOSTSFILE='/etc/hosts' BAKFILE='%s.sbak' % HOSTSFILE APPEND='# sshuttle-firewall-%d AUTOCREATED' % port old_content = '' st = None try: old_content = open(HOSTSFILE).read() st = os.stat(HOSTSFILE) except IOError, e: if e.errno == errno.ENOENT: pass else: raise if old_content.strip() and not os.path.exists(BAKFILE): os.link(HOSTSFILE, BAKFILE) tmpname = "%s.%d.tmp" % (HOSTSFILE, port) f = open(tmpname, 'w') for line in old_content.rstrip().split('\n'): if line.find(APPEND) >= 0: continue f.write('%s\n' % line) for (name,ip) in sorted(hostmap.items()): f.write('%-30s %s\n' % ('%s %s' % (ip,name), APPEND)) f.close() if st: os.chown(tmpname, st.st_uid, st.st_gid) os.chmod(tmpname, st.st_mode) else: os.chown(tmpname, 0, 0) os.chmod(tmpname, 0644) os.rename(tmpname, HOSTSFILE) def restore_etc_hosts(port): global hostmap hostmap = {} rewrite_etc_hosts(port) # This is some voodoo for setting up the kernel's transparent # proxying stuff. If subnets is empty, we just delete our sshuttle rules; # otherwise we delete it, then make them from scratch. # # This code is supposed to clean up after itself by deleting its rules on # exit. In case that fails, it's not the end of the world; future runs will # supercede it in the transproxy list, at least, so the leftover rules # are hopefully harmless. def main(port, dnsport, syslog): assert(port > 0) assert(port <= 65535) assert(dnsport >= 0) assert(dnsport <= 65535) if os.getuid() != 0: raise Fatal('you must be root (or enable su/sudo) to set the firewall') if program_exists('ipfw'): do_it = do_ipfw elif program_exists('iptables'): do_it = do_iptables else: raise Fatal("can't find either ipfw or iptables; check your PATH") # because of limitations of the 'su' command, the *real* stdin/stdout # are both attached to stdout initially. Clone stdout into stdin so we # can read from it. os.dup2(1, 0) if syslog: ssyslog.start_syslog() ssyslog.stderr_to_syslog() debug1('firewall manager ready.\n') sys.stdout.write('READY\n') sys.stdout.flush() # ctrl-c shouldn't be passed along to me. When the main sshuttle dies, # I'll die automatically. os.setsid() # we wait until we get some input before creating the rules. That way, # sshuttle can launch us as early as possible (and get sudo password # authentication as early in the startup process as possible). line = sys.stdin.readline(128) if not line: return # parent died; nothing to do subnets = [] if line != 'ROUTES\n': raise Fatal('firewall: expected ROUTES but got %r' % line) while 1: line = sys.stdin.readline(128) if not line: raise Fatal('firewall: expected route but got %r' % line) elif line == 'GO\n': break try: (width,exclude,ip) = line.strip().split(',', 2) except: raise Fatal('firewall: expected route or GO but got %r' % line) subnets.append((int(width), bool(int(exclude)), ip)) try: if line: debug1('firewall manager: starting transproxy.\n') do_wait = do_it(port, dnsport, subnets) sys.stdout.write('STARTED\n') try: sys.stdout.flush() except IOError: # the parent process died for some reason; he's surely been loud # enough, so no reason to report another error return # Now we wait until EOF or any other kind of exception. We need # to stay running so that we don't need a *second* password # authentication at shutdown time - that cleanup is important! while 1: if do_wait: do_wait() line = sys.stdin.readline(128) if line.startswith('HOST '): (name,ip) = line[5:].strip().split(',', 1) hostmap[name] = ip rewrite_etc_hosts(port) elif line: raise Fatal('expected EOF, got %r' % line) else: break finally: try: debug1('firewall manager: undoing changes.\n') except: pass do_it(port, 0, []) restore_etc_hosts(port) apenwarr-sshuttle-fb849f7/helpers.py000066400000000000000000000030051157107071000176460ustar00rootroot00000000000000import sys, os, socket, errno logprefix = '' verbose = 0 def log(s): try: sys.stdout.flush() sys.stderr.write(logprefix + s) sys.stderr.flush() except IOError: # this could happen if stderr gets forcibly disconnected, eg. because # our tty closes. That sucks, but it's no reason to abort the program. pass def debug1(s): if verbose >= 1: log(s) def debug2(s): if verbose >= 2: log(s) def debug3(s): if verbose >= 3: log(s) class Fatal(Exception): pass def list_contains_any(l, sub): for i in sub: if i in l: return True return False def resolvconf_nameservers(): l = [] for line in open('/etc/resolv.conf'): words = line.lower().split() if len(words) >= 2 and words[0] == 'nameserver': l.append(words[1]) return l def resolvconf_random_nameserver(): l = resolvconf_nameservers() if l: if len(l) > 1: # don't import this unless we really need it import random random.shuffle(l) return l[0] else: return '127.0.0.1' def islocal(ip): sock = socket.socket() try: try: sock.bind((ip, 0)) except socket.error, e: if e.args[0] == errno.EADDRNOTAVAIL: return False # not a local IP else: raise finally: sock.close() return True # it's a local IP, or there would have been an error apenwarr-sshuttle-fb849f7/hostwatch.py000066400000000000000000000161531157107071000202200ustar00rootroot00000000000000import time, socket, re, select, errno if not globals().get('skip_imports'): import compat.ssubprocess as ssubprocess import helpers from helpers import * POLL_TIME = 60*15 NETSTAT_POLL_TIME = 30 CACHEFILE=os.path.expanduser('~/.sshuttle.hosts') _nmb_ok = True _smb_ok = True hostnames = {} queue = {} try: null = open('/dev/null', 'wb') except IOError, e: log('warning: %s\n' % e) null = os.popen("sh -c 'while read x; do :; done'", 'wb', 4096) def _is_ip(s): return re.match(r'\d+\.\d+\.\d+\.\d+$', s) def write_host_cache(): tmpname = '%s.%d.tmp' % (CACHEFILE, os.getpid()) try: f = open(tmpname, 'wb') for name,ip in sorted(hostnames.items()): f.write('%s,%s\n' % (name, ip)) f.close() os.rename(tmpname, CACHEFILE) finally: try: os.unlink(tmpname) except: pass def read_host_cache(): try: f = open(CACHEFILE) except IOError, e: if e.errno == errno.ENOENT: return else: raise for line in f: words = line.strip().split(',') if len(words) == 2: (name,ip) = words name = re.sub(r'[^-\w]', '-', name).strip() ip = re.sub(r'[^0-9.]', '', ip).strip() if name and ip: found_host(name, ip) def found_host(hostname, ip): hostname = re.sub(r'\..*', '', hostname) hostname = re.sub(r'[^-\w]', '_', hostname) if (ip.startswith('127.') or ip.startswith('255.') or hostname == 'localhost'): return oldip = hostnames.get(hostname) if oldip != ip: hostnames[hostname] = ip debug1('Found: %s: %s\n' % (hostname, ip)) sys.stdout.write('%s,%s\n' % (hostname, ip)) write_host_cache() def _check_etc_hosts(): debug2(' > hosts\n') for line in open('/etc/hosts'): line = re.sub(r'#.*', '', line) words = line.strip().split() if not words: continue ip = words[0] names = words[1:] if _is_ip(ip): debug3('< %s %r\n' % (ip, names)) for n in names: check_host(n) found_host(n, ip) def _check_revdns(ip): debug2(' > rev: %s\n' % ip) try: r = socket.gethostbyaddr(ip) debug3('< %s\n' % r[0]) check_host(r[0]) found_host(r[0], ip) except socket.herror, e: pass def _check_dns(hostname): debug2(' > dns: %s\n' % hostname) try: ip = socket.gethostbyname(hostname) debug3('< %s\n' % ip) check_host(ip) found_host(hostname, ip) except socket.gaierror, e: pass def _check_netstat(): debug2(' > netstat\n') argv = ['netstat', '-n'] try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) content = p.stdout.read() p.wait() except OSError, e: log('%r failed: %r\n' % (argv, e)) return for ip in re.findall(r'\d+\.\d+\.\d+\.\d+', content): debug3('< %s\n' % ip) check_host(ip) def _check_smb(hostname): return global _smb_ok if not _smb_ok: return argv = ['smbclient', '-U', '%', '-L', hostname] debug2(' > smb: %s\n' % hostname) try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) lines = p.stdout.readlines() p.wait() except OSError, e: log('%r failed: %r\n' % (argv, e)) _smb_ok = False return lines.reverse() # junk at top while lines: line = lines.pop().strip() if re.match(r'Server\s+', line): break # server list section: # Server Comment # ------ ------- while lines: line = lines.pop().strip() if not line or re.match(r'-+\s+-+', line): continue if re.match(r'Workgroup\s+Master', line): break words = line.split() hostname = words[0].lower() debug3('< %s\n' % hostname) check_host(hostname) # workgroup list section: # Workgroup Master # --------- ------ while lines: line = lines.pop().strip() if re.match(r'-+\s+', line): continue if not line: break words = line.split() (workgroup, hostname) = (words[0].lower(), words[1].lower()) debug3('< group(%s) -> %s\n' % (workgroup, hostname)) check_host(hostname) check_workgroup(workgroup) if lines: assert(0) def _check_nmb(hostname, is_workgroup, is_master): return global _nmb_ok if not _nmb_ok: return argv = ['nmblookup'] + ['-M']*is_master + ['--', hostname] debug2(' > n%d%d: %s\n' % (is_workgroup, is_master, hostname)) try: p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE, stderr=null) lines = p.stdout.readlines() rv = p.wait() except OSError, e: log('%r failed: %r\n' % (argv, e)) _nmb_ok = False return if rv: log('%r returned %d\n' % (argv, rv)) return for line in lines: m = re.match(r'(\d+\.\d+\.\d+\.\d+) (\w+)<\w\w>\n', line) if m: g = m.groups() (ip, name) = (g[0], g[1].lower()) debug3('< %s -> %s\n' % (name, ip)) if is_workgroup: _enqueue(_check_smb, ip) else: found_host(name, ip) check_host(name) def check_host(hostname): if _is_ip(hostname): _enqueue(_check_revdns, hostname) else: _enqueue(_check_dns, hostname) _enqueue(_check_smb, hostname) _enqueue(_check_nmb, hostname, False, False) def check_workgroup(hostname): _enqueue(_check_nmb, hostname, True, False) _enqueue(_check_nmb, hostname, True, True) def _enqueue(op, *args): t = (op,args) if queue.get(t) == None: queue[t] = 0 def _stdin_still_ok(timeout): r,w,x = select.select([sys.stdin.fileno()], [], [], timeout) if r: b = os.read(sys.stdin.fileno(), 4096) if not b: return False return True def hw_main(seed_hosts): if helpers.verbose >= 2: helpers.logprefix = 'HH: ' else: helpers.logprefix = 'hostwatch: ' read_host_cache() _enqueue(_check_etc_hosts) _enqueue(_check_netstat) check_host('localhost') check_host(socket.gethostname()) check_workgroup('workgroup') check_workgroup('-') for h in seed_hosts: check_host(h) while 1: now = time.time() for t,last_polled in queue.items(): (op,args) = t if not _stdin_still_ok(0): break maxtime = POLL_TIME if op == _check_netstat: maxtime = NETSTAT_POLL_TIME if now - last_polled > maxtime: queue[t] = time.time() op(*args) try: sys.stdout.flush() except IOError: break # FIXME: use a smarter timeout based on oldest last_polled if not _stdin_still_ok(1): break apenwarr-sshuttle-fb849f7/main.py000066400000000000000000000113401157107071000171310ustar00rootroot00000000000000import sys, os, re import helpers, options, client, server, firewall, hostwatch import compat.ssubprocess as ssubprocess from helpers import * # list of: # 1.2.3.4/5 or just 1.2.3.4 def parse_subnets(subnets_str): subnets = [] for s in subnets_str: m = re.match(r'(\d+)(?:\.(\d+)\.(\d+)\.(\d+))?(?:/(\d+))?$', s) if not m: raise Fatal('%r is not a valid IP subnet format' % s) (a,b,c,d,width) = m.groups() (a,b,c,d) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0)) if width == None: width = 32 else: width = int(width) if a > 255 or b > 255 or c > 255 or d > 255: raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d)) if width > 32: raise Fatal('*/%d is greater than the maximum of 32' % width) subnets.append(('%d.%d.%d.%d' % (a,b,c,d), width)) return subnets # 1.2.3.4:567 or just 1.2.3.4 or just 567 def parse_ipport(s): s = str(s) m = re.match(r'(?:(\d+)\.(\d+)\.(\d+)\.(\d+))?(?::)?(?:(\d+))?$', s) if not m: raise Fatal('%r is not a valid IP:port format' % s) (a,b,c,d,port) = m.groups() (a,b,c,d,port) = (int(a or 0), int(b or 0), int(c or 0), int(d or 0), int(port or 0)) if a > 255 or b > 255 or c > 255 or d > 255: raise Fatal('%d.%d.%d.%d has numbers > 255' % (a,b,c,d)) if port > 65535: raise Fatal('*:%d is greater than the maximum of 65535' % port) if a == None: a = b = c = d = 0 return ('%d.%d.%d.%d' % (a,b,c,d), port) optspec = """ sshuttle [-l [ip:]port] [-r [username@]sshserver[:port]] sshuttle --server sshuttle --firewall sshuttle --hostwatch -- l,listen= transproxy to this ip address and port number [127.0.0.1:0] H,auto-hosts scan for remote hostnames and update local /etc/hosts N,auto-nets automatically determine subnets to route dns capture local DNS requests and forward to the remote DNS server python= path to python interpreter on the remote server r,remote= ssh hostname (and optional username) of remote sshuttle server x,exclude= exclude this subnet (can be used more than once) v,verbose increase debug message verbosity e,ssh-cmd= the command to use to connect to the remote [ssh] seed-hosts= with -H, use these hostnames for initial scan (comma-separated) no-latency-control sacrifice latency to improve bandwidth benchmarks wrap= restart counting channel numbers after this number (for testing) D,daemon run in the background as a daemon syslog send log messages to syslog (default if you use --daemon) pidfile= pidfile name (only if using --daemon) [./sshuttle.pid] server (internal use only) firewall (internal use only) hostwatch (internal use only) """ o = options.Options(optspec) (opt, flags, extra) = o.parse(sys.argv[2:]) if opt.daemon: opt.syslog = 1 if opt.wrap: import ssnet ssnet.MAX_CHANNEL = int(opt.wrap) helpers.verbose = opt.verbose try: if opt.server: if len(extra) != 0: o.fatal('no arguments expected') server.latency_control = opt.latency_control sys.exit(server.main()) elif opt.firewall: if len(extra) != 2: o.fatal('exactly two arguments expected') sys.exit(firewall.main(int(extra[0]), int(extra[1]), opt.syslog)) elif opt.hostwatch: sys.exit(hostwatch.hw_main(extra)) else: if len(extra) < 1 and not opt.auto_nets: o.fatal('at least one subnet (or -N) expected') includes = extra excludes = ['127.0.0.0/8'] for k,v in flags: if k in ('-x','--exclude'): excludes.append(v) remotename = opt.remote if remotename == '' or remotename == '-': remotename = None if opt.seed_hosts and not opt.auto_hosts: o.fatal('--seed-hosts only works if you also use -H') if opt.seed_hosts: sh = re.split(r'[\s,]+', (opt.seed_hosts or "").strip()) elif opt.auto_hosts: sh = [] else: sh = None sys.exit(client.main(parse_ipport(opt.listen or '0.0.0.0:0'), opt.ssh_cmd, remotename, opt.python, opt.latency_control, opt.dns, sh, opt.auto_nets, parse_subnets(includes), parse_subnets(excludes), opt.syslog, opt.daemon, opt.pidfile)) except Fatal, e: log('fatal: %s\n' % e) sys.exit(99) except KeyboardInterrupt: log('\n') log('Keyboard interrupt: exiting.\n') sys.exit(1) apenwarr-sshuttle-fb849f7/options.py000066400000000000000000000146621157107071000177120ustar00rootroot00000000000000"""Command-line options parser. With the help of an options spec string, easily parse command-line options. """ import sys, os, textwrap, getopt, re, struct class OptDict: def __init__(self): self._opts = {} def __setitem__(self, k, v): if k.startswith('no-') or k.startswith('no_'): k = k[3:] v = not v self._opts[k] = v def __getitem__(self, k): if k.startswith('no-') or k.startswith('no_'): return not self._opts[k[3:]] return self._opts[k] def __getattr__(self, k): return self[k] def _default_onabort(msg): sys.exit(97) def _intify(v): try: vv = int(v or '') if str(vv) == v: return vv except ValueError: pass return v def _atoi(v): try: return int(v or 0) except ValueError: return 0 def _remove_negative_kv(k, v): if k.startswith('no-') or k.startswith('no_'): return k[3:], not v return k,v def _remove_negative_k(k): return _remove_negative_kv(k, None)[0] def _tty_width(): s = struct.pack("HHHH", 0, 0, 0, 0) try: import fcntl, termios s = fcntl.ioctl(sys.stderr.fileno(), termios.TIOCGWINSZ, s) except (IOError, ImportError): return _atoi(os.environ.get('WIDTH')) or 70 (ysize,xsize,ypix,xpix) = struct.unpack('HHHH', s) return xsize or 70 class Options: """Option parser. When constructed, two strings are mandatory. The first one is the command name showed before error messages. The second one is a string called an optspec that specifies the synopsis and option flags and their description. For more information about optspecs, consult the bup-options(1) man page. Two optional arguments specify an alternative parsing function and an alternative behaviour on abort (after having output the usage string). By default, the parser function is getopt.gnu_getopt, and the abort behaviour is to exit the program. """ def __init__(self, optspec, optfunc=getopt.gnu_getopt, onabort=_default_onabort): self.optspec = optspec self._onabort = onabort self.optfunc = optfunc self._aliases = {} self._shortopts = 'h?' self._longopts = ['help'] self._hasparms = {} self._defaults = {} self._usagestr = self._gen_usage() def _gen_usage(self): out = [] lines = self.optspec.strip().split('\n') lines.reverse() first_syn = True while lines: l = lines.pop() if l == '--': break out.append('%s: %s\n' % (first_syn and 'usage' or ' or', l)) first_syn = False out.append('\n') last_was_option = False while lines: l = lines.pop() if l.startswith(' '): out.append('%s%s\n' % (last_was_option and '\n' or '', l.lstrip())) last_was_option = False elif l: (flags, extra) = l.split(' ', 1) extra = extra.strip() if flags.endswith('='): flags = flags[:-1] has_parm = 1 else: has_parm = 0 g = re.search(r'\[([^\]]*)\]$', extra) if g: defval = g.group(1) else: defval = None flagl = flags.split(',') flagl_nice = [] for _f in flagl: f,dvi = _remove_negative_kv(_f, _intify(defval)) self._aliases[f] = _remove_negative_k(flagl[0]) self._hasparms[f] = has_parm self._defaults[f] = dvi if len(f) == 1: self._shortopts += f + (has_parm and ':' or '') flagl_nice.append('-' + f) else: f_nice = re.sub(r'\W', '_', f) self._aliases[f_nice] = _remove_negative_k(flagl[0]) self._longopts.append(f + (has_parm and '=' or '')) self._longopts.append('no-' + f) flagl_nice.append('--' + _f) flags_nice = ', '.join(flagl_nice) if has_parm: flags_nice += ' ...' prefix = ' %-20s ' % flags_nice argtext = '\n'.join(textwrap.wrap(extra, width=_tty_width(), initial_indent=prefix, subsequent_indent=' '*28)) out.append(argtext + '\n') last_was_option = True else: out.append('\n') last_was_option = False return ''.join(out).rstrip() + '\n' def usage(self, msg=""): """Print usage string to stderr and abort.""" sys.stderr.write(self._usagestr) e = self._onabort and self._onabort(msg) or None if e: raise e def fatal(self, s): """Print an error message to stderr and abort with usage string.""" msg = 'error: %s\n' % s sys.stderr.write(msg) return self.usage(msg) def parse(self, args): """Parse a list of arguments and return (options, flags, extra). In the returned tuple, "options" is an OptDict with known options, "flags" is a list of option flags that were used on the command-line, and "extra" is a list of positional arguments. """ try: (flags,extra) = self.optfunc(args, self._shortopts, self._longopts) except getopt.GetoptError, e: self.fatal(e) opt = OptDict() for k,v in self._defaults.iteritems(): k = self._aliases[k] opt[k] = v for (k,v) in flags: k = k.lstrip('-') if k in ('h', '?', 'help'): self.usage() if k.startswith('no-'): k = self._aliases[k[3:]] v = 0 else: k = self._aliases[k] if not self._hasparms[k]: assert(v == '') v = (opt._opts.get(k) or 0) + 1 else: v = _intify(v) opt[k] = v for (f1,f2) in self._aliases.iteritems(): opt[f1] = opt._opts.get(f2) return (opt,flags,extra) apenwarr-sshuttle-fb849f7/server.py000066400000000000000000000164421157107071000175230ustar00rootroot00000000000000import re, struct, socket, select, traceback, time if not globals().get('skip_imports'): import ssnet, helpers, hostwatch import compat.ssubprocess as ssubprocess from ssnet import SockWrapper, Handler, Proxy, Mux, MuxWrapper from helpers import * def _ipmatch(ipstr): if ipstr == 'default': ipstr = '0.0.0.0/0' m = re.match(r'^(\d+(\.\d+(\.\d+(\.\d+)?)?)?)(?:/(\d+))?$', ipstr) if m: g = m.groups() ips = g[0] width = int(g[4] or 32) if g[1] == None: ips += '.0.0.0' width = min(width, 8) elif g[2] == None: ips += '.0.0' width = min(width, 16) elif g[3] == None: ips += '.0' width = min(width, 24) return (struct.unpack('!I', socket.inet_aton(ips))[0], width) def _ipstr(ip, width): if width >= 32: return ip else: return "%s/%d" % (ip, width) def _maskbits(netmask): if not netmask: return 32 for i in range(32): if netmask[0] & _shl(1, i): return 32-i return 0 def _shl(n, bits): return n * int(2**bits) def _list_routes(): argv = ['netstat', '-rn'] p = ssubprocess.Popen(argv, stdout=ssubprocess.PIPE) routes = [] for line in p.stdout: cols = re.split(r'\s+', line) ipw = _ipmatch(cols[0]) if not ipw: continue # some lines won't be parseable; never mind maskw = _ipmatch(cols[2]) # linux only mask = _maskbits(maskw) # returns 32 if maskw is null width = min(ipw[1], mask) ip = ipw[0] & _shl(_shl(1, width) - 1, 32-width) routes.append((socket.inet_ntoa(struct.pack('!I', ip)), width)) rv = p.wait() if rv != 0: log('WARNING: %r returned %d\n' % (argv, rv)) log('WARNING: That prevents --auto-nets from working.\n') return routes def list_routes(): for (ip,width) in _list_routes(): if not ip.startswith('0.') and not ip.startswith('127.'): yield (ip,width) def _exc_dump(): exc_info = sys.exc_info() return ''.join(traceback.format_exception(*exc_info)) def start_hostwatch(seed_hosts): s1,s2 = socket.socketpair() pid = os.fork() if not pid: # child rv = 99 try: try: s2.close() os.dup2(s1.fileno(), 1) os.dup2(s1.fileno(), 0) s1.close() rv = hostwatch.hw_main(seed_hosts) or 0 except Exception, e: log('%s\n' % _exc_dump()) rv = 98 finally: os._exit(rv) s1.close() return pid,s2 class Hostwatch: def __init__(self): self.pid = 0 self.sock = None class DnsProxy(Handler): def __init__(self, mux, chan, request): sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) Handler.__init__(self, [sock]) self.timeout = time.time()+30 self.mux = mux self.chan = chan self.tries = 0 self.peer = None self.request = request self.sock = sock self.sock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) self.try_send() def try_send(self): if self.tries >= 3: return self.tries += 1 self.peer = resolvconf_random_nameserver() self.sock.connect((self.peer, 53)) debug2('DNS: sending to %r\n' % self.peer) try: self.sock.send(self.request) except socket.error, e: if e.args[0] in ssnet.NET_ERRS: # might have been spurious; try again. # Note: these errors sometimes are reported by recv(), # and sometimes by send(). We have to catch both. debug2('DNS send to %r: %s\n' % (self.peer, e)) self.try_send() return else: log('DNS send to %r: %s\n' % (self.peer, e)) return def callback(self): try: data = self.sock.recv(4096) except socket.error, e: if e.args[0] in ssnet.NET_ERRS: # might have been spurious; try again. # Note: these errors sometimes are reported by recv(), # and sometimes by send(). We have to catch both. debug2('DNS recv from %r: %s\n' % (self.peer, e)) self.try_send() return else: log('DNS recv from %r: %s\n' % (self.peer, e)) return debug2('DNS response: %d bytes\n' % len(data)) self.mux.send(self.chan, ssnet.CMD_DNS_RESPONSE, data) self.ok = False def main(): if helpers.verbose >= 1: helpers.logprefix = ' s: ' else: helpers.logprefix = 'server: ' debug1('latency control setting = %r\n' % latency_control) routes = list(list_routes()) debug1('available routes:\n') for r in routes: debug1(' %s/%d\n' % r) # synchronization header sys.stdout.write('\0\0SSHUTTLE0001') sys.stdout.flush() handlers = [] mux = Mux(socket.fromfd(sys.stdin.fileno(), socket.AF_INET, socket.SOCK_STREAM), socket.fromfd(sys.stdout.fileno(), socket.AF_INET, socket.SOCK_STREAM)) handlers.append(mux) routepkt = '' for r in routes: routepkt += '%s,%d\n' % r mux.send(0, ssnet.CMD_ROUTES, routepkt) hw = Hostwatch() hw.leftover = '' def hostwatch_ready(): assert(hw.pid) content = hw.sock.recv(4096) if content: lines = (hw.leftover + content).split('\n') if lines[-1]: # no terminating newline: entry isn't complete yet! hw.leftover = lines.pop() lines.append('') else: hw.leftover = '' mux.send(0, ssnet.CMD_HOST_LIST, '\n'.join(lines)) else: raise Fatal('hostwatch process died') def got_host_req(data): if not hw.pid: (hw.pid,hw.sock) = start_hostwatch(data.strip().split()) handlers.append(Handler(socks = [hw.sock], callback = hostwatch_ready)) mux.got_host_req = got_host_req def new_channel(channel, data): (dstip,dstport) = data.split(',', 1) dstport = int(dstport) outwrap = ssnet.connect_dst(dstip,dstport) handlers.append(Proxy(MuxWrapper(mux, channel), outwrap)) mux.new_channel = new_channel dnshandlers = {} def dns_req(channel, data): debug2('Incoming DNS request.\n') h = DnsProxy(mux, channel, data) handlers.append(h) dnshandlers[channel] = h mux.got_dns_req = dns_req while mux.ok: if hw.pid: assert(hw.pid > 0) (rpid, rv) = os.waitpid(hw.pid, os.WNOHANG) if rpid: raise Fatal('hostwatch exited unexpectedly: code 0x%04x\n' % rv) ssnet.runonce(handlers, mux) if latency_control: mux.check_fullness() mux.callback() if dnshandlers: now = time.time() for channel,h in dnshandlers.items(): if h.timeout < now or not h.ok: del dnshandlers[channel] h.ok = False apenwarr-sshuttle-fb849f7/ssh.py000066400000000000000000000065411157107071000170110ustar00rootroot00000000000000import sys, os, re, socket, zlib import compat.ssubprocess as ssubprocess import helpers from helpers import * def readfile(name): basedir = os.path.dirname(os.path.abspath(sys.argv[0])) path = [basedir] + sys.path for d in path: fullname = os.path.join(d, name) if os.path.exists(fullname): return open(fullname, 'rb').read() raise Exception("can't find file %r in any of %r" % (name, path)) def empackage(z, filename, data=None): (path,basename) = os.path.split(filename) if not data: data = readfile(filename) content = z.compress(data) content += z.flush(zlib.Z_SYNC_FLUSH) return '%s\n%d\n%s' % (basename, len(content), content) def connect(ssh_cmd, rhostport, python, stderr, options): main_exe = sys.argv[0] portl = [] rhostIsIPv6 = False if (rhostport or '').count(':') > 1: rhostIsIPv6 = True if rhostport.count(']') or rhostport.count('['): result = rhostport.split(']') rhost = result[0].strip('[') if len(result) > 1: result[1] = result[1].strip(':') if result[1] is not '': portl = ['-p', str(int(result[1]))] else: # can't disambiguate IPv6 colons and a port number. pass the hostname through. rhost = rhostport else: # IPv4 l = (rhostport or '').split(':', 1) rhost = l[0] if len(l) > 1: portl = ['-p', str(int(l[1]))] if rhost == '-': rhost = None ipv6flag = [] if rhostIsIPv6: ipv6flag = ['-6'] z = zlib.compressobj(1) content = readfile('assembler.py') optdata = ''.join("%s=%r\n" % (k,v) for (k,v) in options.items()) content2 = (empackage(z, 'cmdline_options.py', optdata) + empackage(z, 'helpers.py') + empackage(z, 'compat/ssubprocess.py') + empackage(z, 'ssnet.py') + empackage(z, 'hostwatch.py') + empackage(z, 'server.py') + "\n") pyscript = r""" import sys; skip_imports=1; verbosity=%d; exec compile(sys.stdin.read(%d), "assembler.py", "exec") """ % (helpers.verbose or 0, len(content)) pyscript = re.sub(r'\s+', ' ', pyscript.strip()) if not rhost: # ignore the --python argument when running locally; we already know # which python version works. argv = [sys.argv[1], '-c', pyscript] else: if ssh_cmd: sshl = ssh_cmd.split(' ') else: sshl = ['ssh'] if python: pycmd = "'%s' -c '%s'" % (python, pyscript) else: pycmd = ("P=python2; $P -V 2>/dev/null || P=python; " "exec \"$P\" -c '%s'") % pyscript argv = (sshl + portl + ipv6flag + [rhost, '--', pycmd]) (s1,s2) = socket.socketpair() def setup(): # runs in the child process s2.close() s1a,s1b = os.dup(s1.fileno()), os.dup(s1.fileno()) s1.close() debug2('executing: %r\n' % argv) p = ssubprocess.Popen(argv, stdin=s1a, stdout=s1b, preexec_fn=setup, close_fds=True, stderr=stderr) os.close(s1a) os.close(s1b) s2.sendall(content) s2.sendall(content2) return p, s2 apenwarr-sshuttle-fb849f7/sshuttle000077500000000000000000000003701157107071000174350ustar00rootroot00000000000000#!/bin/sh EXE=$0 for i in 1 2 3 4 5 6 7 8 9 10; do [ -L "$EXE" ] || break EXE=$(readlink "$EXE") done DIR=$(dirname "$EXE") if python2 -V 2>/dev/null; then exec python2 "$DIR/main.py" python2 "$@" else exec python "$DIR/main.py" python "$@" fi apenwarr-sshuttle-fb849f7/sshuttle.md000066400000000000000000000232201157107071000200300ustar00rootroot00000000000000% sshuttle(8) Sshuttle 0.46 % Avery Pennarun % 2011-01-25 # NAME sshuttle - a transparent proxy-based VPN using ssh # SYNOPSIS sshuttle [options...] [-r [username@]sshserver[:port]] \ # DESCRIPTION sshuttle allows you to create a VPN connection from your machine to any remote server that you can connect to via ssh, as long as that server has python 2.3 or higher. To work, you must have root access on the local machine, but you can have a normal account on the server. It's valid to run sshuttle more than once simultaneously on a single client machine, connecting to a different server every time, so you can be on more than one VPN at once. If run on a router, sshuttle can forward traffic for your entire subnet to the VPN. # OPTIONS \ : a list of subnets to route over the VPN, in the form `a.b.c.d[/width]`. Valid examples are 1.2.3.4 (a single IP address), 1.2.3.4/32 (equivalent to 1.2.3.4), 1.2.3.0/24 (a 24-bit subnet, ie. with a 255.255.255.0 netmask), and 0/0 ('just route everything through the VPN'). -l, --listen=*[ip:]port* : use this ip address and port number as the transparent proxy port. By default sshuttle finds an available port automatically and listens on IP 127.0.0.1 (localhost), so you don't need to override it, and connections are only proxied from the local machine, not from outside machines. If you want to accept connections from other machines on your network (ie. to run sshuttle on a router) try enabling IP Forwarding in your kernel, then using `--listen 0.0.0.0:0`. -H, --auto-hosts : scan for remote hostnames and update the local /etc/hosts file with matching entries for as long as the VPN is open. This is nicer than changing your system's DNS (/etc/resolv.conf) settings, for several reasons. First, hostnames are added without domain names attached, so you can `ssh thatserver` without worrying if your local domain matches the remote one. Second, if you sshuttle into more than one VPN at a time, it's impossible to use more than one DNS server at once anyway, but sshuttle correctly merges /etc/hosts entries between all running copies. Third, if you're only routing a few subnets over the VPN, you probably would prefer to keep using your local DNS server for everything else. -N, --auto-nets : in addition to the subnets provided on the command line, ask the server which subnets it thinks we should route, and route those automatically. The suggestions are taken automatically from the server's routing table. --python : specify the name/path of the remote python interpreter. The default is just `python`, which means to use the default python interpreter on the remote system's PATH. -r, --remote=*[username@]sshserver[:port]* : the remote hostname and optional username and ssh port number to use for connecting to the remote server. For example, example.com, testuser@example.com, testuser@example.com:2222, or example.com:2244. -x, --exclude=*subnet* : explicitly exclude this subnet from forwarding. The format of this option is the same as the `` option. To exclude more than one subnet, specify the `-x` option more than once. You can say something like `0/0 -x 1.2.3.0/24` to forward everything except the local subnet over the VPN, for example. -v, --verbose : print more information about the session. This option can be used more than once for increased verbosity. By default, sshuttle prints only error messages. -e, --ssh-cmd : the command to use to connect to the remote server. The default is just `ssh`. Use this if your ssh client is in a non-standard location or you want to provide extra options to the ssh command, for example, `-e 'ssh -v'`. --seed-hosts : a comma-separated list of hostnames to use to initialize the `--auto-hosts` scan algorithm. `--auto-hosts` does things like poll local SMB servers for lists of local hostnames, but can speed things up if you use this option to give it a few names to start from. --no-latency-control : sacrifice latency to improve bandwidth benchmarks. ssh uses really big socket buffers, which can overload the connection if you start doing large file transfers, thus making all your other sessions inside the same tunnel go slowly. Normally, sshuttle tries to avoid this problem using a "fullness check" that allows only a certain amount of outstanding data to be buffered at a time. But on high-bandwidth links, this can leave a lot of your bandwidth underutilized. It also makes sshuttle seem slow in bandwidth benchmarks (benchmarks rarely test ping latency, which is what sshuttle is trying to control). This option disables the latency control feature, maximizing bandwidth usage. Use at your own risk. -D, --daemon : automatically fork into the background after connecting to the remote server. Implies `--syslog`. --syslog : after connecting, send all log messages to the `syslog`(3) service instead of stderr. This is implicit if you use `--daemon`. --pidfile=*pidfilename* : when using `--daemon`, save sshuttle's pid to *pidfilename*. The default is `sshuttle.pid` in the current directory. --server : (internal use only) run the sshuttle server on stdin/stdout. This is what the client runs on the remote end. --firewall : (internal use only) run the firewall manager. This is the only part of sshuttle that must run as root. If you start sshuttle as a non-root user, it will automatically run `sudo` or `su` to start the firewall manager, but the core of sshuttle still runs as a normal user. --hostwatch : (internal use only) run the hostwatch daemon. This process runs on the server side and collects hostnames for the `--auto-hosts` option. Using this option by itself makes it a lot easier to debug and test the `--auto-hosts` feature. # EXAMPLES Test locally by proxying all local connections, without using ssh: $ sshuttle -v 0/0 Starting sshuttle proxy. Listening on ('0.0.0.0', 12300). [local sudo] Password: firewall manager ready. c : connecting to server... s: available routes: s: 192.168.42.0/24 c : connected. firewall manager: starting transproxy. c : Accept: 192.168.42.106:50035 -> 192.168.42.121:139. c : Accept: 192.168.42.121:47523 -> 77.141.99.22:443. ...etc... ^C firewall manager: undoing changes. KeyboardInterrupt c : Keyboard interrupt: exiting. c : SW#8:192.168.42.121:47523: deleting c : SW#6:192.168.42.106:50035: deleting Test connection to a remote server, with automatic hostname and subnet guessing: $ sshuttle -vNHr example.org Starting sshuttle proxy. Listening on ('0.0.0.0', 12300). firewall manager ready. c : connecting to server... s: available routes: s: 77.141.99.0/24 c : connected. c : seed_hosts: [] firewall manager: starting transproxy. hostwatch: Found: testbox1: 1.2.3.4 hostwatch: Found: mytest2: 5.6.7.8 hostwatch: Found: domaincontroller: 99.1.2.3 c : Accept: 192.168.42.121:60554 -> 77.141.99.22:22. ^C firewall manager: undoing changes. c : Keyboard interrupt: exiting. c : SW#6:192.168.42.121:60554: deleting # DISCUSSION When it starts, sshuttle creates an ssh session to the server specified by the `-r` option. If `-r` is omitted, it will start both its client and server locally, which is sometimes useful for testing. After connecting to the remote server, sshuttle uploads its (python) source code to the remote end and executes it there. Thus, you don't need to install sshuttle on the remote server, and there are never sshuttle version conflicts between client and server. Unlike most VPNs, sshuttle forwards sessions, not packets. That is, it uses kernel transparent proxying (`iptables REDIRECT` rules on Linux, or `ipfw fwd` rules on BSD) to capture outgoing TCP sessions, then creates entirely separate TCP sessions out to the original destination at the other end of the tunnel. Packet-level forwarding (eg. using the tun/tap devices on Linux) seems elegant at first, but it results in several problems, notably the 'tcp over tcp' problem. The tcp protocol depends fundamentally on packets being dropped in order to implement its congestion control agorithm; if you pass tcp packets through a tcp-based tunnel (such as ssh), the inner tcp packets will never be dropped, and so the inner tcp stream's congestion control will be completely broken, and performance will be terrible. Thus, packet-based VPNs (such as IPsec and openvpn) cannot use tcp-based encrypted streams like ssh or ssl, and have to implement their own encryption from scratch, which is very complex and error prone. sshuttle's simplicity comes from the fact that it can safely use the existing ssh encrypted tunnel without incurring a performance penalty. It does this by letting the client-side kernel manage the incoming tcp stream, and the server-side kernel manage the outgoing tcp stream; there is no need for congestion control to be shared between the two separate streams, so a tcp-based tunnel is fine. # BUGS On MacOS 10.6 (at least up to 10.6.6), your network will stop responding about 10 minutes after the first time you start sshuttle, because of a MacOS kernel bug relating to arp and the net.inet.ip.scopedroute sysctl. To fix it, just switch your wireless off and on. Sshuttle makes the kernel setting it changes permanent, so this won't happen again, even after a reboot. # SEE ALSO `ssh`(1), `python`(1) apenwarr-sshuttle-fb849f7/ssnet.py000066400000000000000000000430231157107071000173440ustar00rootroot00000000000000import struct, socket, errno, select if not globals().get('skip_imports'): from helpers import * MAX_CHANNEL = 65535 # these don't exist in the socket module in python 2.3! SHUT_RD = 0 SHUT_WR = 1 SHUT_RDWR = 2 HDR_LEN = 8 CMD_EXIT = 0x4200 CMD_PING = 0x4201 CMD_PONG = 0x4202 CMD_CONNECT = 0x4203 CMD_STOP_SENDING = 0x4204 CMD_EOF = 0x4205 CMD_DATA = 0x4206 CMD_ROUTES = 0x4207 CMD_HOST_REQ = 0x4208 CMD_HOST_LIST = 0x4209 CMD_DNS_REQ = 0x420a CMD_DNS_RESPONSE = 0x420b cmd_to_name = { CMD_EXIT: 'EXIT', CMD_PING: 'PING', CMD_PONG: 'PONG', CMD_CONNECT: 'CONNECT', CMD_STOP_SENDING: 'STOP_SENDING', CMD_EOF: 'EOF', CMD_DATA: 'DATA', CMD_ROUTES: 'ROUTES', CMD_HOST_REQ: 'HOST_REQ', CMD_HOST_LIST: 'HOST_LIST', CMD_DNS_REQ: 'DNS_REQ', CMD_DNS_RESPONSE: 'DNS_RESPONSE', } NET_ERRS = [errno.ECONNREFUSED, errno.ETIMEDOUT, errno.EHOSTUNREACH, errno.ENETUNREACH, errno.EHOSTDOWN, errno.ENETDOWN] def _add(l, elem): if not elem in l: l.append(elem) def _fds(l): out = [] for i in l: try: out.append(i.fileno()) except AttributeError: out.append(i) out.sort() return out def _nb_clean(func, *args): try: return func(*args) except OSError, e: if e.errno not in (errno.EWOULDBLOCK, errno.EAGAIN): raise else: debug3('%s: err was: %s\n' % (func.__name__, e)) return None def _try_peername(sock): try: pn = sock.getpeername() if pn: return '%s:%s' % (pn[0], pn[1]) except socket.error, e: if e.args[0] not in (errno.ENOTCONN, errno.ENOTSOCK): raise return 'unknown' _swcount = 0 class SockWrapper: def __init__(self, rsock, wsock, connect_to=None, peername=None): global _swcount _swcount += 1 debug3('creating new SockWrapper (%d now exist)\n' % _swcount) self.exc = None self.rsock = rsock self.wsock = wsock self.shut_read = self.shut_write = False self.buf = [] self.connect_to = connect_to self.peername = peername or _try_peername(self.rsock) self.try_connect() def __del__(self): global _swcount _swcount -= 1 debug1('%r: deleting (%d remain)\n' % (self, _swcount)) if self.exc: debug1('%r: error was: %s\n' % (self, self.exc)) def __repr__(self): if self.rsock == self.wsock: fds = '#%d' % self.rsock.fileno() else: fds = '#%d,%d' % (self.rsock.fileno(), self.wsock.fileno()) return 'SW%s:%s' % (fds, self.peername) def seterr(self, e): if not self.exc: self.exc = e self.nowrite() self.noread() def try_connect(self): if self.connect_to and self.shut_write: self.noread() self.connect_to = None if not self.connect_to: return # already connected self.rsock.setblocking(False) debug3('%r: trying connect to %r\n' % (self, self.connect_to)) if socket.inet_aton(self.connect_to[0])[0] == '\0': self.seterr(Exception("Can't connect to %r: " "IP address starts with zero\n" % (self.connect_to,))) self.connect_to = None return try: self.rsock.connect(self.connect_to) # connected successfully (Linux) self.connect_to = None except socket.error, e: debug3('%r: connect result: %s\n' % (self, e)) if e.args[0] == errno.EINVAL: # this is what happens when you call connect() on a socket # that is now connected but returned EINPROGRESS last time, # on BSD, on python pre-2.5.1. We need to use getsockopt() # to get the "real" error. Later pythons do this # automatically, so this code won't run. realerr = self.rsock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) e = socket.error(realerr, os.strerror(realerr)) debug3('%r: fixed connect result: %s\n' % (self, e)) if e.args[0] in [errno.EINPROGRESS, errno.EALREADY]: pass # not connected yet elif e.args[0] == 0: # connected successfully (weird Linux bug?) # Sometimes Linux seems to return EINVAL when it isn't # invalid. This *may* be caused by a race condition # between connect() and getsockopt(SO_ERROR) (ie. it # finishes connecting in between the two, so there is no # longer an error). However, I'm not sure of that. # # I did get at least one report that the problem went away # when we added this, however. self.connect_to = None elif e.args[0] == errno.EISCONN: # connected successfully (BSD) self.connect_to = None elif e.args[0] in NET_ERRS + [errno.EACCES, errno.EPERM]: # a "normal" kind of error self.connect_to = None self.seterr(e) else: raise # error we've never heard of?! barf completely. def noread(self): if not self.shut_read: debug2('%r: done reading\n' % self) self.shut_read = True #self.rsock.shutdown(SHUT_RD) # doesn't do anything anyway def nowrite(self): if not self.shut_write: debug2('%r: done writing\n' % self) self.shut_write = True try: self.wsock.shutdown(SHUT_WR) except socket.error, e: self.seterr('nowrite: %s' % e) def too_full(self): return False # fullness is determined by the socket's select() state def uwrite(self, buf): if self.connect_to: return 0 # still connecting self.wsock.setblocking(False) try: return _nb_clean(os.write, self.wsock.fileno(), buf) except OSError, e: if e.errno == errno.EPIPE: debug1('%r: uwrite: got EPIPE\n' % self) self.nowrite() return 0 else: # unexpected error... stream is dead self.seterr('uwrite: %s' % e) return 0 def write(self, buf): assert(buf) return self.uwrite(buf) def uread(self): if self.connect_to: return None # still connecting if self.shut_read: return self.rsock.setblocking(False) try: return _nb_clean(os.read, self.rsock.fileno(), 65536) except OSError, e: self.seterr('uread: %s' % e) return '' # unexpected error... we'll call it EOF def fill(self): if self.buf: return rb = self.uread() if rb: self.buf.append(rb) if rb == '': # empty string means EOF; None means temporarily empty self.noread() def copy_to(self, outwrap): if self.buf and self.buf[0]: wrote = outwrap.write(self.buf[0]) self.buf[0] = self.buf[0][wrote:] while self.buf and not self.buf[0]: self.buf.pop(0) if not self.buf and self.shut_read: outwrap.nowrite() class Handler: def __init__(self, socks = None, callback = None): self.ok = True self.socks = socks or [] if callback: self.callback = callback def pre_select(self, r, w, x): for i in self.socks: _add(r, i) def callback(self): log('--no callback defined-- %r\n' % self) (r,w,x) = select.select(self.socks, [], [], 0) for s in r: v = s.recv(4096) if not v: log('--closed-- %r\n' % self) self.socks = [] self.ok = False class Proxy(Handler): def __init__(self, wrap1, wrap2): Handler.__init__(self, [wrap1.rsock, wrap1.wsock, wrap2.rsock, wrap2.wsock]) self.wrap1 = wrap1 self.wrap2 = wrap2 def pre_select(self, r, w, x): if self.wrap1.shut_write: self.wrap2.noread() if self.wrap2.shut_write: self.wrap1.noread() if self.wrap1.connect_to: _add(w, self.wrap1.rsock) elif self.wrap1.buf: if not self.wrap2.too_full(): _add(w, self.wrap2.wsock) elif not self.wrap1.shut_read: _add(r, self.wrap1.rsock) if self.wrap2.connect_to: _add(w, self.wrap2.rsock) elif self.wrap2.buf: if not self.wrap1.too_full(): _add(w, self.wrap1.wsock) elif not self.wrap2.shut_read: _add(r, self.wrap2.rsock) def callback(self): self.wrap1.try_connect() self.wrap2.try_connect() self.wrap1.fill() self.wrap2.fill() self.wrap1.copy_to(self.wrap2) self.wrap2.copy_to(self.wrap1) if self.wrap1.buf and self.wrap2.shut_write: self.wrap1.buf = [] self.wrap1.noread() if self.wrap2.buf and self.wrap1.shut_write: self.wrap2.buf = [] self.wrap2.noread() if (self.wrap1.shut_read and self.wrap2.shut_read and not self.wrap1.buf and not self.wrap2.buf): self.ok = False self.wrap1.nowrite() self.wrap2.nowrite() class Mux(Handler): def __init__(self, rsock, wsock): Handler.__init__(self, [rsock, wsock]) self.rsock = rsock self.wsock = wsock self.new_channel = self.got_dns_req = self.got_routes = None self.got_host_req = self.got_host_list = None self.channels = {} self.chani = 0 self.want = 0 self.inbuf = '' self.outbuf = [] self.fullness = 0 self.too_full = False self.send(0, CMD_PING, 'chicken') def next_channel(self): # channel 0 is special, so we never allocate it for timeout in xrange(1024): self.chani += 1 if self.chani > MAX_CHANNEL: self.chani = 1 if not self.channels.get(self.chani): return self.chani def amount_queued(self): total = 0 for b in self.outbuf: total += len(b) return total def check_fullness(self): if self.fullness > 32768: if not self.too_full: self.send(0, CMD_PING, 'rttest') self.too_full = True #ob = [] #for b in self.outbuf: # (s1,s2,c) = struct.unpack('!ccH', b[:4]) # ob.append(c) #log('outbuf: %d %r\n' % (self.amount_queued(), ob)) def send(self, channel, cmd, data): data = str(data) assert(len(data) <= 65535) p = struct.pack('!ccHHH', 'S', 'S', channel, cmd, len(data)) + data self.outbuf.append(p) debug2(' > channel=%d cmd=%s len=%d (fullness=%d)\n' % (channel, cmd_to_name.get(cmd,hex(cmd)), len(data), self.fullness)) self.fullness += len(data) def got_packet(self, channel, cmd, data): debug2('< channel=%d cmd=%s len=%d\n' % (channel, cmd_to_name.get(cmd,hex(cmd)), len(data))) if cmd == CMD_PING: self.send(0, CMD_PONG, data) elif cmd == CMD_PONG: debug2('received PING response\n') self.too_full = False self.fullness = 0 elif cmd == CMD_EXIT: self.ok = False elif cmd == CMD_CONNECT: assert(not self.channels.get(channel)) if self.new_channel: self.new_channel(channel, data) elif cmd == CMD_DNS_REQ: assert(not self.channels.get(channel)) if self.got_dns_req: self.got_dns_req(channel, data) elif cmd == CMD_ROUTES: if self.got_routes: self.got_routes(data) else: raise Exception('got CMD_ROUTES without got_routes?') elif cmd == CMD_HOST_REQ: if self.got_host_req: self.got_host_req(data) else: raise Exception('got CMD_HOST_REQ without got_host_req?') elif cmd == CMD_HOST_LIST: if self.got_host_list: self.got_host_list(data) else: raise Exception('got CMD_HOST_LIST without got_host_list?') else: callback = self.channels.get(channel) if not callback: log('warning: closed channel %d got cmd=%s len=%d\n' % (channel, cmd_to_name.get(cmd,hex(cmd)), len(data))) else: callback(cmd, data) def flush(self): self.wsock.setblocking(False) if self.outbuf and self.outbuf[0]: wrote = _nb_clean(os.write, self.wsock.fileno(), self.outbuf[0]) debug2('mux wrote: %r/%d\n' % (wrote, len(self.outbuf[0]))) if wrote: self.outbuf[0] = self.outbuf[0][wrote:] while self.outbuf and not self.outbuf[0]: self.outbuf[0:1] = [] def fill(self): self.rsock.setblocking(False) try: b = _nb_clean(os.read, self.rsock.fileno(), 32768) except OSError, e: raise Fatal('other end: %r' % e) #log('<<< %r\n' % b) if b == '': # EOF self.ok = False if b: self.inbuf += b def handle(self): self.fill() #log('inbuf is: (%d,%d) %r\n' # % (self.want, len(self.inbuf), self.inbuf)) while 1: if len(self.inbuf) >= (self.want or HDR_LEN): (s1,s2,channel,cmd,datalen) = \ struct.unpack('!ccHHH', self.inbuf[:HDR_LEN]) assert(s1 == 'S') assert(s2 == 'S') self.want = datalen + HDR_LEN if self.want and len(self.inbuf) >= self.want: data = self.inbuf[HDR_LEN:self.want] self.inbuf = self.inbuf[self.want:] self.want = 0 self.got_packet(channel, cmd, data) else: break def pre_select(self, r, w, x): _add(r, self.rsock) if self.outbuf: _add(w, self.wsock) def callback(self): (r,w,x) = select.select([self.rsock], [self.wsock], [], 0) if self.rsock in r: self.handle() if self.outbuf and self.wsock in w: self.flush() class MuxWrapper(SockWrapper): def __init__(self, mux, channel): SockWrapper.__init__(self, mux.rsock, mux.wsock) self.mux = mux self.channel = channel self.mux.channels[channel] = self.got_packet self.socks = [] debug2('new channel: %d\n' % channel) def __del__(self): self.nowrite() SockWrapper.__del__(self) def __repr__(self): return 'SW%r:Mux#%d' % (self.peername,self.channel) def noread(self): if not self.shut_read: self.shut_read = True self.mux.send(self.channel, CMD_STOP_SENDING, '') self.maybe_close() def nowrite(self): if not self.shut_write: self.shut_write = True self.mux.send(self.channel, CMD_EOF, '') self.maybe_close() def maybe_close(self): if self.shut_read and self.shut_write: # remove the mux's reference to us. The python garbage collector # will then be able to reap our object. self.mux.channels[self.channel] = None def too_full(self): return self.mux.too_full def uwrite(self, buf): if self.mux.too_full: return 0 # too much already enqueued if len(buf) > 2048: buf = buf[:2048] self.mux.send(self.channel, CMD_DATA, buf) return len(buf) def uread(self): if self.shut_read: return '' # EOF else: return None # no data available right now def got_packet(self, cmd, data): if cmd == CMD_EOF: self.noread() elif cmd == CMD_STOP_SENDING: self.nowrite() elif cmd == CMD_DATA: self.buf.append(data) else: raise Exception('unknown command %d (%d bytes)' % (cmd, len(data))) def connect_dst(ip, port): debug2('Connecting to %s:%d\n' % (ip, port)) outsock = socket.socket() outsock.setsockopt(socket.SOL_IP, socket.IP_TTL, 42) return SockWrapper(outsock, outsock, connect_to = (ip,port), peername = '%s:%d' % (ip,port)) def runonce(handlers, mux): r = [] w = [] x = [] to_remove = filter(lambda s: not s.ok, handlers) for h in to_remove: handlers.remove(h) for s in handlers: s.pre_select(r,w,x) debug2('Waiting: %d r=%r w=%r x=%r (fullness=%d/%d)\n' % (len(handlers), _fds(r), _fds(w), _fds(x), mux.fullness, mux.too_full)) (r,w,x) = select.select(r,w,x) debug2(' Ready: %d r=%r w=%r x=%r\n' % (len(handlers), _fds(r), _fds(w), _fds(x))) ready = r+w+x did = {} for h in handlers: for s in h.socks: if s in ready: h.callback() did[s] = 1 for s in ready: if not s in did: raise Fatal('socket %r was not used by any handler' % s) apenwarr-sshuttle-fb849f7/ssyslog.py000066400000000000000000000005461157107071000177160ustar00rootroot00000000000000import sys, os from compat import ssubprocess _p = None def start_syslog(): global _p _p = ssubprocess.Popen(['logger', '-p', 'daemon.notice', '-t', 'sshuttle'], stdin=ssubprocess.PIPE) def stderr_to_syslog(): sys.stdout.flush() sys.stderr.flush() os.dup2(_p.stdin.fileno(), 2) apenwarr-sshuttle-fb849f7/stresstest.py000077500000000000000000000047751157107071000204510ustar00rootroot00000000000000#!/usr/bin/env python import sys, os, socket, select, struct, time listener = socket.socket() listener.bind(('127.0.0.1', 0)) listener.listen(500) servers = [] clients = [] remain = {} NUMCLIENTS = 50 count = 0 while 1: if len(clients) < NUMCLIENTS: c = socket.socket() c.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) c.bind(('0.0.0.0', 0)) c.connect(listener.getsockname()) count += 1 if count >= 16384: count = 1 print 'cli CREATING %d' % count b = struct.pack('I', count) + 'x'*count remain[c] = count print 'cli >> %r' % len(b) c.send(b) c.shutdown(socket.SHUT_WR) clients.append(c) r = [listener] time.sleep(0.1) else: r = [listener]+servers+clients print 'select(%d)' % len(r) r,w,x = select.select(r, [], [], 5) assert(r) for i in r: if i == listener: s,addr = listener.accept() servers.append(s) elif i in servers: b = i.recv(4096) print 'srv << %r' % len(b) if not i in remain: assert(len(b) >= 4) want = struct.unpack('I', b[:4])[0] b = b[4:] #i.send('y'*want) else: want = remain[i] if want < len(b): print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b) assert(want >= len(b)) want -= len(b) remain[i] = want if not b: # EOF if want: print 'weird: eof but wanted %d more' % want assert(want == 0) i.close() servers.remove(i) del remain[i] else: print 'srv >> %r' % len(b) i.send('y'*len(b)) if not want: i.shutdown(socket.SHUT_WR) elif i in clients: b = i.recv(4096) print 'cli << %r' % len(b) want = remain[i] if want < len(b): print 'weird wanted %d bytes, got %d: %r' % (want, len(b), b) assert(want >= len(b)) want -= len(b) remain[i] = want if not b: # EOF if want: print 'weird: eof but wanted %d more' % want assert(want == 0) i.close() clients.remove(i) del remain[i] listener.accept() apenwarr-sshuttle-fb849f7/ui-macos/000077500000000000000000000000001157107071000173515ustar00rootroot00000000000000apenwarr-sshuttle-fb849f7/ui-macos/.gitignore000066400000000000000000000001141157107071000213350ustar00rootroot00000000000000*.pyc *~ /*.nib /debug.app /sources.list /Sshuttle VPN.app /*.tar.gz /*.zip apenwarr-sshuttle-fb849f7/ui-macos/Info.plist000066400000000000000000000022151157107071000213210ustar00rootroot00000000000000 CFBundleDevelopmentRegion English CFBundleDisplayName Sshuttle VPN CFBundleExecutable Sshuttle CFBundleIconFile app.icns CFBundleIdentifier ca.apenwarr.Sshuttle CFBundleInfoDictionaryVersion 6.0 CFBundleName Sshuttle VPN CFBundlePackageType APPL CFBundleShortVersionString 0.0.0 CFBundleSignature ???? CFBundleVersion 0.0.0 LSUIElement 1 LSHasLocalizedDisplayName NSAppleScriptEnabled NSHumanReadableCopyright GNU LGPL Version 2 NSMainNibFile MainMenu NSPrincipalClass NSApplication apenwarr-sshuttle-fb849f7/ui-macos/MainMenu.xib000066400000000000000000003555311157107071000216020ustar00rootroot00000000000000 1060 10J567 762 1038.35 462.00 com.apple.InterfaceBuilder.CocoaPlugin 762 YES YES com.apple.InterfaceBuilder.CocoaPlugin YES YES YES YES NSApplication FirstResponder NSApplication 11 2 {{324, 171}, {611, 469}} 1886913536 Sshuttle VPN Preferences NSWindow View {1.79769e+308, 1.79769e+308} {611, 469} 274 YES 18 {{-8, -10}, {627, 473}} 2 YES 1 274 YES 292 {{17, 17}, {25, 23}} 2 YES 67239424 134217728 LucidaGrande 13 1044 -2033958657 134 NSImage NSAddTemplate 400 75 292 {{41, 17}, {25, 23}} 2 YES 67239424 134217728 -2033958657 134 NSImage NSRemoveTemplate 400 75 274 YES 2304 YES 256 {224, 372} 2 YES -2147483392 {{224, 0}, {16, 17}} YES 22 10 3.4028234663852886e+38 75628096 2048 Connect LucidaGrande 11 3100 6 System headerColor 3 MQA 6 System headerTextColor 3 MAA 67239424 0 Check 1215582719 2 NSImage NSSwitch NSSwitch 200 25 YES 196 40 1000 75628096 2048 Server 3 MC4zMzMzMzI5ODU2AA 67239488 272631808 Text 6 System controlColor 3 MC42NjY2NjY2NjY3AA 6 System controlTextColor 1 YES 3 2 6 System controlBackgroundColor 6 System gridColor 3 MC41AA 19 48234496 0 15 0 YES 0 {{1, 1}, {223, 372}} 2 4 -2147483392 {{143, 1}, {15, 274}} 2 _doScroller: 0.99636363636363634 -2147483392 {{-100, -100}, {157, 15}} 2 YES 1 _doScroller: 0.91812865497076024 {{17, 40}, {225, 374}} 2 658 QSAAAEEgAABBqAAAQagAAA 265 {{291, 356}, {273, 18}} 2 YES -2080244224 0 Automatically detect remote hostnames 1211912703 2 200 25 265 {{341, 392}, {249, 22}} 2 YES -1804468671 272630848 YES 6 System textBackgroundColor 6 System textColor 265 {{273, 193}, {320, 26}} 2 YES -2076049856 2048 112869631 129 400 75 Choose one 1048576 2147483647 1 NSImage NSMenuCheckmark NSImage NSMenuMixedState _popUpItemAction: YES OtherViews YES Item 2 1048576 2147483647 _popUpItemAction: Item 3 1048576 2147483647 _popUpItemAction: 1 YES YES 2 265 {{273, 257}, {320, 26}} 2 YES -2076049856 2048 112869631 129 400 75 Choose one 1048576 2147483647 1 _popUpItemAction: YES OtherViews YES Item 2 1048576 2147483647 _popUpItemAction: Item 3 1048576 2147483647 _popUpItemAction: 1 YES YES 2 265 {{261, 394}, {75, 17}} 2 YES 68288064 272630784 SSH Server: 265 {{261, 226}, {105, 17}} 2 YES 68288064 272630784 Network routes: 265 {{261, 290}, {105, 17}} 2 YES 68288064 272630784 Optimize for: 289 {{276, 40}, {25, 23}} 2 YES 67239424 134217728 -2033958657 134 400 75 289 {{300, 40}, {25, 23}} 2 YES 67239424 134217728 -2033958657 134 400 75 273 YES 2304 YES 256 {312, 102} 2 YES 256 {312, 17} 2 -2147483392 {{224, 0}, {16, 17}} 2 YES 134 40 1000 75628096 2048 Subnet 3 MC4zMzMzMzI5ODU2AA 337772096 2048 255.255.255.255 3 YES YES 172 40 1000 75628096 2048 Width 337772096 2048 Text Cell 3 YES YES 3 2 17 1379926016 4 15 0 YES 0 {{1, 17}, {312, 102}} 2 4 -2147483392 {{224, 17}, {15, 102}} 2 _doScroller: 0.96938775510204078 -2147483392 {{1, 75}, {141, 15}} 2 1 _doScroller: 0.98734177215189878 2304 YES {{1, 0}, {312, 17}} 2 4 {{276, 63}, {314, 120}} 2 562 QSAAAEEgAABBmAAAQZgAAA 265 {{291, 323}, {285, 18}} 2 YES -2080244224 0 Send DNS requests through this server 1211912703 2 200 25 {{10, 33}, {607, 427}} 2 SSH Servers 2 274 YES 269 {{195, 374}, {236, 29}} 2 YES 604110336 0 Start Sshuttle at login 1211912703 2 200 25 269 {{195, 343}, {236, 29}} 2 YES -2080244224 0 Auto-reconnect if disconnected 1211912703 2 200 25 269 {{195, 312}, {236, 29}} 2 YES 67239424 0 Enable debug messages 1211912703 2 200 25 {{10, 33}, {607, 427}} 2 Options Log Messages 256 YES 274 YES 2304 YES 2322 {596, 114} 2 YES 134 596 1 10497 0 YES YES NSBackgroundColor NSColor YES 6 System selectedTextBackgroundColor 6 System selectedTextColor YES YES NSColor NSCursor NSUnderline YES 1 MCAwIDEAA {8, -8} 13 6 {600, 1e+07} {223, 11} {{1, 1}, {596, 410}} 2 {4, -5} 1 4 256 {{597, 1}, {15, 410}} 2 _doScroller: 1 0.85256409645080566 256 {{1, 411}, {596, 15}} 2 1 _doScroller: 1 0.94565218687057495 {{-3, -3}, {613, 427}} 2 50 {{10, 33}, {607, 427}} 2 Log Messages 0 YES YES {611, 469} subviews YES fromTop fade YES filter subtype type 2 {{0, 0}, {1280, 778}} {611, 491} {1.79769e+308, 1.79769e+308} YES YES YES YES YES YES SshuttleController YES SshuttleApp YES host autoHosts autoNets nets netsHidden wantConnect statusMsg status isValid useDns title latencyControl SshuttleServer YES YES YES YES YES YES YES subnet width SshuttleNet YES YES YES YES YES YES YES startAtLoginField 250 prefsWindow 254 value: values.startAtLogin value: values.startAtLogin value values.startAtLogin 2 309 delegate 312 add: 337 remove: 338 routingField 435 value: values.autoReconnect value: values.autoReconnect value values.autoReconnect 2 480 selectedIndex: selection.autoNets selectedIndex: selection.autoNets selectedIndex selection.autoNets NSNoSelectionPlaceholder 2 501 value: selection.autoHosts value: selection.autoHosts value selection.autoHosts NSNoSelectionPlaceholder 2 502 value: selection.host value: selection.host value selection.host YES YES NSAllowsEditingMultipleValuesSelection NSContinuouslyUpdatesValue NSNoSelectionPlaceholder NSNullPlaceholder NSValidatesImmediately YES Click [+] to add a new server username@hostname.domain.com 2 503 add: 525 remove: 526 contentArray: selection.nets contentArray: selection.nets contentArray selection.nets 2 528 hidden: selection.netsHidden hidden: selection.netsHidden hidden selection.netsHidden NSNoSelectionPlaceholder 2 535 hidden: selection.netsHidden hidden: selection.netsHidden hidden selection.netsHidden NSNoSelectionPlaceholder 2 538 hidden: selection.netsHidden hidden: selection.netsHidden hidden selection.netsHidden NSNoSelectionPlaceholder 2 541 value: arrangedObjects.subnet value: arrangedObjects.subnet value arrangedObjects.subnet YES YES NSNullPlaceholder NSValidatesImmediately YES a.b.c.d 2 543 value: arrangedObjects.width value: arrangedObjects.width value arrangedObjects.width NSValidatesImmediately 2 544 contentArray: servers contentArray: servers contentArray servers NSDeletesObjectsOnRemove 2 545 serversController 547 logField 549 value: arrangedObjects.wantConnect value: arrangedObjects.wantConnect value arrangedObjects.wantConnect 2 550 autoReconnectField 551 value: values.debug value: values.debug value values.debug 2 555 debugField 556 enabled: arrangedObjects.isValid enabled: arrangedObjects.isValid enabled arrangedObjects.isValid 2 557 value: selection.useDns value: selection.useDns value selection.useDns NSNoSelectionPlaceholder 2 572 value: arrangedObjects.title value: arrangedObjects.title value arrangedObjects.title YES YES NSConditionallySetsEditable NSNullPlaceholder YES Untitled 2 573 selectedIndex: selection.latencyControl selectedIndex: selection.latencyControl selectedIndex selection.latencyControl NSNoSelectionPlaceholder 2 581 latencyControlField 582 YES 0 -2 File's Owner -1 First Responder 21 YES Window 2 YES 207 SshuttleController -3 Application 233 YES 234 YES 235 YES 236 YES 237 YES 242 YES 243 224 YES Servers 225 226 227 YES 229 YES 275 YES 276 277 YES 278 300 YES 301 YES 302 YES 303 304 305 307 310 334 ServersArray 370 YES 371 372 YES 373 424 YES 425 YES 426 YES 427 428 429 430 RouteList 431 YES 432 433 YES 434 476 YES 477 486 504 YES 506 509 YES Nets 510 511 512 YES 513 514 YES 515 YES 516 517 518 YES 519 YES 520 521 524 NetsArray 552 YES 553 561 YES 562 574 YES 575 YES 576 YES 577 578 579 583 YES 584 YES YES -3.IBPluginDependency 2.IBPluginDependency 2.ImportedFromIB2 207.ImportedFromIB2 21.IBEditorWindowLastContentRect 21.IBPluginDependency 21.IBWindowTemplateEditedContentRect 21.ImportedFromIB2 21.NSWindowTemplate.visibleAtLaunch 21.windowTemplate.hasMinSize 21.windowTemplate.minSize 224.IBAttributePlaceholdersKey 224.IBPluginDependency 225.IBPluginDependency 226.IBPluginDependency 227.IBPluginDependency 229.IBPluginDependency 233.IBAttributePlaceholdersKey 233.IBPluginDependency 233.IBViewIntegration.shadowBlurRadius 233.IBViewIntegration.shadowColor 233.IBViewIntegration.shadowOffsetHeight 233.IBViewIntegration.shadowOffsetWidth 234.IBPluginDependency 235.IBPluginDependency 236.IBPluginDependency 237.IBPluginDependency 242.IBAttributePlaceholdersKey 242.IBPluginDependency 243.IBPluginDependency 275.IBAttributePlaceholdersKey 275.IBPluginDependency 276.IBPluginDependency 277.IBAttributePlaceholdersKey 277.IBPluginDependency 278.IBPluginDependency 300.IBPluginDependency 301.IBPluginDependency 302.IBPluginDependency 303.IBPluginDependency 304.IBPluginDependency 305.IBPluginDependency 307.IBPluginDependency 334.IBPluginDependency 370.IBAttributePlaceholdersKey 370.IBPluginDependency 371.IBPluginDependency 372.IBAttributePlaceholdersKey 372.IBPluginDependency 373.IBPluginDependency 424.IBAttributePlaceholdersKey 424.IBPluginDependency 425.IBPluginDependency 426.IBPluginDependency 427.IBPluginDependency 428.IBPluginDependency 429.IBPluginDependency 430.IBPluginDependency 431.IBPluginDependency 432.IBPluginDependency 433.IBPluginDependency 434.IBPluginDependency 476.IBAttributePlaceholdersKey 476.IBPluginDependency 477.IBPluginDependency 486.IBPluginDependency 504.IBPluginDependency 506.IBPluginDependency 509.IBPluginDependency 510.IBPluginDependency 511.IBPluginDependency 512.IBPluginDependency 513.IBPluginDependency 514.IBPluginDependency 515.IBPluginDependency 516.IBPluginDependency 517.IBPluginDependency 518.IBAttributePlaceholdersKey 518.IBPluginDependency 519.IBAttributePlaceholdersKey 519.IBPluginDependency 520.IBPluginDependency 521.IBPluginDependency 524.IBPluginDependency 552.IBAttributePlaceholdersKey 552.IBPluginDependency 553.IBPluginDependency 561.IBAttributePlaceholdersKey 561.IBPluginDependency 562.IBPluginDependency 574.IBAttributePlaceholdersKey 574.IBPluginDependency 575.IBPluginDependency 576.IBPluginDependency 577.IBPluginDependency 578.IBPluginDependency 579.IBPluginDependency 583.IBPluginDependency 584.IBPluginDependency YES com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin {{324, 171}, {611, 469}} com.apple.InterfaceBuilder.CocoaPlugin {{324, 171}, {611, 469}} {611, 469} ToolTip ToolTip The list of configured servers. To add a new one, click the [+] button below. com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin InitialTabViewItem InitialTabViewItem com.apple.InterfaceBuilder.CocoaPlugin 1 MC4xMDg2OTU2NTIyIDAuMTA4Njk1NjUyMiAwLjEwODY5NTY1MjIgMC42NwA com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip Do you want to make the Sshuttle menu icon visible immediately after logging in, or only if you launch Sshuttle manually? com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip Add a server com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip Delete the current server com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip Search for server names on the remote end and add them to your computer's /etc/hosts file. com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip The name of the remote ssh server, in username@hostname:port format. username@ and :port are optional. The remote server needs to have at least python 2.3 installed, but does not need a copy of sshuttle. com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip Choose which network traffic should be routed over the VPN. com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip If your connection to the server dies, do you want Sshuttle to retry the connection right away? com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip Add a server com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip Delete the current server com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip If your connection to the server dies, do you want Sshuttle to retry the connection right away? com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip Search for server names on the remote end and add them to your computer's /etc/hosts file. com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin ToolTip ToolTip Choose which network traffic should be routed over the VPN. com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin YES YES YES YES 584 YES FirstResponder NSObject IBUserSource SshuttleApp NSObject IBUserSource SshuttleController NSObject YES YES autoReconnectField debugField latencyControlField logField prefsWindow routingField serversController startAtLoginField YES id id id id id id id id IBUserSource 0 IBCocoaFramework com.apple.InterfaceBuilder.CocoaPlugin.macosx com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 YES 3 YES YES NSAddTemplate NSMenuCheckmark NSMenuMixedState NSRemoveTemplate NSSwitch YES {8, 8} {9, 8} {7, 2} {8, 8} {15, 15} apenwarr-sshuttle-fb849f7/ui-macos/UserDefaults.plist000066400000000000000000000004021157107071000230300ustar00rootroot00000000000000 startAtLogin autoReconnect apenwarr-sshuttle-fb849f7/ui-macos/all.do000066400000000000000000000000351157107071000204430ustar00rootroot00000000000000redo-ifchange debug.app dist apenwarr-sshuttle-fb849f7/ui-macos/app.icns000066400000000000000000003274071157107071000210240ustar00rootroot00000000000000icnsic09 jP ftypjp2 jp2 Ojp2hihdrcolr"cdefjp2cOQ2R \ PXX`XX`XX`XXXPPXdKakadu-v5.2.1 b>T3u1Aw̲cԄi;~sJeABATq*c7 OG0%n RZ(Lio@0I6|d<=Ynt{$Cif\˪F)I9yZKbkS=a/(i!(PO( ~a1a=͆EKQj0_M|IY7DK/ 4tQPhIӠVxY;ju]I8GfӶhi]nzMbˡW-Es !ާ2U;9P8\2:NJ>I uZQ Ň%!Rt)+3>`\ ț ?3xL ]vR/chEDYc(%ELm{%w+@'vM*ΫY!v@Ұ7-}Ѭ8ɲ*xջR˚0$ЗrE>pty*VOڙkeE$|uuPH`AZ3bH%rœ$wbu 9ikʳq}97'S!E>vSa R*Z(|8!h3Hڡږx7Y YˢYebvZv^ws:3;}zz^Hu(U_pV+<'MfjtQ@OU~@~ldcLC-#Cu\X& BOg{|}ѸhI,BN0ruPV{l}X`aIcZAٷSҸm%NDZk>T?(@'?炌Xh^:g`iOP.JVxԛ r:]~5X6Mx{ YݕXىNâY'm`3KZWv*.LGnZ`7 '\U; #޻ºC\!5qL[|<"r=G YF4b0)dLyu6UzRBEmf-AV~PV~~q(l?k,ڢ;~qknI6@XBb5NkHi L> -XW4N!ԒɄJ?m5ےb6jؘ oz(#0b&-kOѯ18FLݦu9H-0oeBY=&ԵADőSgJVz΀^gK5E:sZ炝ܩ* +7Ml'F6ܖ`T"g#m*$:rn8W=03si6#hKiOmtX?">gİYz%v'WF)M(hR³$irh>iU1R #iv[g)<Գ#LLj6@ % Kc> ܀ΘIM>@ _}3SWvдI(;>O7P1r~hu"U?Ҥ")yavM5|răW˟ n ~&hOyzYmZM鞗y,Atx+`V͚ʦFMݷg͊ tt<>GkES@E&fqLMoώ=~0kâl+CIC7z2!܊6+ 1!M'[Pr??e~ [>Z\`N:e}O38, _Qege+~?87GC 'G9иmJK/,U)C45Ǽݽ୒m䭅Õ)߅Wc~ s8~\'*!-*^ѐ7*N\pÕV=Oaz+DW375\8tڂΧ7o })̴ܝ ;b7:-Us!<oe#Fq'C YzMR߸Ni /dyhWt÷qD 4CgШ}#F|V<^?fOR1&APaYG6ua۴'T%Z,g"**)y.8yjiSoկ2kFH*#0O 94&oV۲ 1!y v>tS4dih-SdňyOi/1Kȑ᧻2@ MP%U)<3-ŝˋ J>~qk5bTy&úE~l%(u7X* ?:?3n}/?ذ3a?@^6z.p} 9pvdt^~ s%-OStr_`!ȴq􊡥L[t5\p#~ݛL Ҏ?}.uGt#M[w3`1{ʧu\xi'j)׾٧ߑy90[ZFQ^a3.(??$];RW5%a/G5JP1!fש_ҧcyI<3^\yiwbBBMSZ].A#;_N}]?m6 ?NⱫ.!q^+nz2{N]q;*?qn_=jc1ZvF <-p&?`D3EBV?̓6%'qw O+P80Y[Ȋ1Tl7U.tSK|A@luq4yw`ݥފҵu`_1e1"ۉ<=9F V]h*uUxZI#D;}6h!<OHQݽ9|)[|߱bw'?DAV=;K,Ϙ)uw8)&K5qB^&D^,װs otpA?Ž$a" 4QO%R]1S*YQ1c>dW4,k#rFNAcdTC9QC1W=$tR:g[Bv@ϳmv Äb[&_fxh4Z{o7!9HeL (ooc<6 )S܍{ N7<dt`MRt1ϓrmAŦ"HS&a^4P1D:k{.a*.oH`4هCwJI,l %CE yʇޜ|` hej{ fo^i,<vvHbP|6 Zー O cM)_\' RK3m:ǀWa(: ͒1-ho4%{*GsѼQww.Ƣw'?AL|-S/?1kK)q=gg'#!垽#޳㓨<~"%n\=2)픲x+˕96"K@pڱH9̔ysR(a<7\Jߐ4!đz@Z2 х }kN $lf @dRri`&2xJCHs7:BE༡&s qtlcGxxE/ 'i6;N,E8|$Ƽ>]6! \J )E7UTO a_F\_-FS EQ;`ZFRjl=7_qS^ۯ8=^kxN0hN?qWF?X.6j&N>ylTwl^)J}%)#-^UVË/cpotX@ĘϠr%e|N&E3ݥ$~f袣,F tf7Ծ,aqzjJVAmINw"U&F9eM뷇6 ɘd$5e"r !kMާ#.(fד̈́n Hч=RYrv~FL!?RvgṼ`dsʩR݈ޜD!{X? k''_zM*|w@F6dO#DO֠Ѵylx#ЪQd|n1.n,J~<*P|m3%17YA1}&̡U \lqg tZP/k5](d6.U\*H{jeѼb7:zUF:+RBVvF3Ƽ<1,xzgr:_hf_O99Ai5XeS+R3(g=垞?fzt&8aGɺ?u<*xہ+4_}1SѬ”Hi9r0mSb~@ȪtIM~=&1%S!docP:+U!=8 ;w))debw-t@UiD'M%իՏx}D\p8ÓyHXl&a"V}M0`Cפ k(I[vxnFKe$}e7dUz0v3o(Q÷CbQJ,']e4}=2UwmE3OA}E ?N6S@A::1ۡ(8P-.:x6:9#wz\8G OH0J`Ck&H7"K@\M~ ug(^1{ǺX=y*w47|}@geMs=gȄllATAHqd:Ȯ="VpCy RXv->Ǔ;#CC!JNfss7p09h/.J.ӣ$GĽC410ӳ_.`8U v' *Hȧ)#F3DxŸrɓgpE2EUXBoX x+:d;0?g <|8^Vmݾd%]MЧfgF:&e<6RnHOJ+!l9ץp(`[Wl J>!H ^qԙT.gPgmB‘Kn!)U{7E5|yZy]"9`]jt|k,N֎cgzVֈG~'1ksv}eRGGw| p?M3nLƊ}|nr{.laRq9%:ܩr^/t譧2v~f汉aS}o-2EދEn0adRgQu).Xk(WBB3Q,Y6Or, 6ߵwXT3JГA=ͥ=VK˵Q8}h16gޥ6*(›bיl{)ȅ֦X0I ߸YUnŷBM!+4%V2zF%@d&g=^)\=P$B\4Au%0X fDGִ~ "/fPY۬߾G6`9\6ytE7IzwvZrm/0Ƨ%A_PBR'բmmDF|soU*D\'!jҠ/d$4Iy2<(@Qۤ\Wf IfxCqy1uhHw{8ꆅOOnh]ybGuG4rŀ  cUuOwZjH8o\5p5j! 48e€LDzo{PJ$IkaS&ei!_]ez\NBϪ%^i$}HyG:߾^ܦZh0l!a}Ndh|XSXk b-(JS$*?o2Z8Wĵ c{B r))>'+7"n>VȦdqn<\;c9eR-'}ơ^4é9FZ߸=T3OV1EԿx5c_kfƨzÒ3M0տZȜ#ӑA e#?%ލ&JfZVFvRg$ٙyrQ%0 6<,KlC{*م>zɜNU|A÷.qp3rtL'1HAYLv4LtR@"@m aҶC I,@X!*ej)AJTRv8؅$Ph0b:AAL")_" ھe#@AZUA3 ˆB>v86o$n$\CxKA܋I7;O#q-0f,\ǧe,^X͝uzawƢe O)f%##|9gl1Hg&hˀ?섾 ;lTYxR=,ө_(ʤ$ڭܷRd<8:D8(ǽć7zky+7+<ݶ ?mc!vPH+i6}&Cqm#8)wKL X'a(C0&{_ Z'\$VdznCccFH^Eqx}&Cg&buEqPXO%i|@ R猜BГ;)4S /R⥏\ UijT*gef>&nJ3{* `p} 'M!+uM/gyd]]nV~H/Cٙv4W&Aq|;zx)"s 9] zqJaNT-B=VaŦQ.T(a7qRۗ %,lsG`_L)c͘s@#Dxe"5ӨVrMĭ@e%r*Ͼ`Vi+'LhƤ.uMu^97)ޕ`Ӱ=STh00@5d$+G).m Q݃WjH9nFb"|C,<xWm0ۉ[=hM#7:WM K1~g_Pokߪ7a PZ7bsάr0ca O̗  ]@/JqMaa#7.% lf+4;2Q],-m p&)t(o/AbpLJRKoC8^ֶJU2:ٸE;T:kU z>lIgugKfP*+r{1!ucG̊![J͚?ƔgEM;@-V={Q1gR׳\/zy7F*[qZnZJl Ο3ZA5L %4 A]>/nhҙN6#Fo*.|d< q;U[+AW~IPo/j-`Fg(4ؿksê&5*v+Mۮ}Rih"Xe[@Q!Q%{ NG9VY &ۈ7k@ (!{S<g]Bjj#)LڛSW/ <;~ A*n_M^B #_gOfE-^]ӫLçB:ڱg""6<䕑F`)n@kIVh1VOr?A-:6ȧWk})G]7X^GN &~/>c#ĸ|@5{ڼ+SlX`,{o|0`>y[7VSRO"P>ag j (Fjsټ7\%mrHdB]lmרi*3&4/DńuHd{1vib W:V$qTE A MlD6CvY=SoH;`Jh<:jP(T}un6x;V ~G\ O(pQG}Bte}w:}_oE&?Jöl rk\{A|;vE'u!ozD: U $8jz: E6܃Q?LyWhJ5#ѯ֓hژjq EjW@hHaic*w79$!?Fp T컄X/4QQ@J#&I!-DVgU60pMdY-EN{?1 $W!7uN[%¥xS-*Q/07DC3턠Sɦuf钣&h[#- t=Nv庁SqyoG4]2 'ml$]]LZ֏>oIM:A6"0ˆ,ʱ%rѽw}M6yHP!et@z\ Ih\}(˖^$KW"9B%Rc~fJXsA=9γt:M=* ZхT<< ;@8frPF3b&{Ůws#703s_$ktʸAZ+ǰ.jp=uDҏ.&eAH<ڿ;Nݲz+9cJU&>L3DHVaU>g0b"/}ks'_:[&֯m.Qjh+ 븩pF *Tcs{4 n~)#x󨦏b쑮-.N߲8\wLn]w&aO+Spmh<`N,b:){Ydh n]`.ȡD>IuL9]qm= OBdɞj# ~ߥ?ؕe@b1;?0 '"7ܙX1~jv+[[kR;X`%o̓;6`R)!(O32fH#%³.d/7OyHS8e*6Lp5vUbCCp86+ܬc~@czՂؒUGsD:Q mY'n\}l\6HDIg)]6`x؋ M/JHs% s4ܛ:JsFM] LQ ;ڋ%>?S84cH;impWc̕]hJb@fy204*!-~\D6RhƊ(kqD:v/^o7؇`ϒ)&S9+o6A[3R/5mi׊4yo7E:>zjCQɂlR-MqoBPtۭ/]وpX6}CVq(뜡 J* Ov὿Üha&[ŀ2"5zo=,߫F‘0 ftG; v"*x a0rvKhs IOZ!do(U΢$"**EJlɻ#?ӂh.;?(t`1K/EȮ&=tQ̲YreEU@g u/̣3o*^ vb.KO[Mfg՘Fddj#ΚE;=[Ei K* b܊ x嵅k#) cJ2x)NF>QmH|7w4絳@߶ W[etUᕱ*+iD^<ْ 2 ]]aao G$hg6KFl { E \׍J)y@"B5K{SLG/p/2 a-Bb}=c&ӛioD eY?O` ҵYMjf1Max{sdޑ-<]Ƈ;4܊5JqfM"n3Xt@s6[Y碆=Clmu{6Iu^<5t?^UBw{IGOggpƉ]T')~Օ`_y2%"B.RTv6G`ƃnzn*4oZ#\T8Fi5)XRbxrڥ۽ 4wn5f`FBD.|K7Էs{Z)2 z,[at[v^k+zJʔ6"&ttF7)uL̶ʤA5zW] #2PYI8NĮ}PvcW:=eJ'4Tݬ2TB`75t`.xHg<&ǔ;u4y|ʝ',2uUWt6 GSq^m(>w3!j,0UB3[ F{ӨםREe*cl{Nj?|VENrWR< .bᑲPc=el =NLd_m%8Ӂݶ0HE Ǘ4{,IFlFfUO rp9谐M$3R@q;nN8v0k/+s}uo.pR"*s|GNLNEgӴPA0y*~,sEUЎTݠM/ қu`59+~='3iD7Ovj$$ \ f4q馶A[mO݌OK{'L}bnm/ަ8ׇz{SY+_ RiYh9b$ I'.|n7w%c#kX1k4J:($HcżPѿ7LK/H9U* t:@_쨒.[%׆JJߒ&3q&w:_WBv 3(@2Nh *V l = f[BkۖswPi?AcBl wf[ B#un=j6}}I[t|&5Swwb(jÉWs.8 5't"΄b 4' $_%\Cӊa7UI;Η-r*[o+{|Qɩ獻PA8ھ f;93+Cºd nMؘ=Fi2}XGu0F5:|[NMhQ[>ٌҔg?^b{] jY4~"c[b ̃-Bb(OW` !Jf>G _R1qg3s,41?WѢUj6:1ne{6]h<c7;J8n (WX$(ɅW@aGj]"bg! A O&N3@Vi`'d(mR^4?KHZ_qZ!z1H<%5d}UßY)uWԙuQm*//9g-Z0ɇ0313=6gZp7TFiYjkQ9Q9T-;X>Ha#ípHDŽͅ\_OڅD\,ޭ8TvN]; `}ğἕ  8` {}nBBu \np>tjqNO]H6{o{y4.jްJNxߕnjϙ}Fj\Ⴈal,JiuzlS{CJՉB#'tF:b >t<Kb)%[ҵH4Okn6%+50X6?T$Ŏ75yg7bףd՘PAR܇G^zvAt:۹^Ϙ@|剕?Nw^3H@7֬tƇ'E64?qqyΡiR77\Ir߭o\٤ҭߎJIi)2]*&ЁTc:z!܇3{w*+s[%͹\|p|Irc5o r׮웂B "]S@ %!ޝD! ,9Md;2`0FO7n[J5m ov(!z7@oZF$CX0C+'Ɛ p|%%\.i,W郂3Ľ}M^7ͧAY)%XIT9.u8&<=&42i6徼\A2 ?Ҹ97 B]us|\Y{k$Iʼ?y^]f1Ba@۳|<‘E]K$][,TBc90p>dPZo6eO ";]ɾyJ]"^x5o*~ OY ^W5XKdu+lݫm-P^`(tVggιH +qH2=3#*u!1nD?SW+$$%TDwTv}ݝwxG=}Ђ]wX+ۺ<>Z%p0':*KGsk'A X.M\dLNULK%ƃت 5ttQ;'}mv}}ӟt%7^2t\2F 8ytG]!hITטS )s o'ܻq/I5x/dzj9ƕkUV' ynapZsiDup1F'ݩ-3{f콷,h=TJfTw;@W^fVS{dsvo%3 q%4tchFTfP:?q'(3:;F0j.>œni^LSZºr3tp }4,2]aaw8+Wu>uqm#@-<:뫓hDTgew`/oeaJϙi\'ݾ#C~ٵ6n@0K njpҖTc*SO=~!-`HF_v$loL+/-) 7OgBr{R #e^XY5!}!9"RɁ:I`AqaX9)YtY ߖ gc6aJЪ٬D?Xjg~WQ}Dub`|}8\Qf MTƝx8ZފD};5<bREfu9 fԠS kޘZiKsf~GC ;#k>ws Ԁ8彖IBԽ+"Rcj|yh`S L_'iوl0J7mA; dn u?|OHu6Icȴ0E43.?T*(TVBF"de-[Ge5 9H`݄aNVfpvL)' q^f"Cj:W1# tdIW腺۱ `NH^0Mrnj1Ɓ`Del}ui`2Oº/R* czd21y^SU77MtQ8>Ly_J{qٞޒJۣS :;PBQAz ֭G`1Ԩ NUp SbZ?<bmfLW9鲏2qns S롪Z6- 0bcmslhyStgDXcNU3__"vOLϴ)9 j+Fo2HmfĢ>c%U<<,"s3q-$b_b*u^\\/AW^t21ѬUZ™X=QJ::0x3&h w3l'RɃ˱B;YR x \h>5m+<,\$Kln.\'ʷA-LZ6iXͽE37Ɣ3}]L@od+*ZZ "«PE,@Mİv6%yq-5Ò?䜺#<`tFO:eWLEqt+|5dzhW:r z&!OSuʵKxBumRlx{ky-M:>CB B(4" 2 i{Ư2n\u4U^#gSSU]$Gte3cAXؾr϶h'u"$Jޥ,ȑoCsA㣸[xx\+?1gRhVh^WRys<[:W6 [ ">vϡ›ܑ7,TS73놾`ay˖߻άb<3(ې7FW-ss|Ye:UNJ7F϶p@h]o[Y4Ӛ3{C 1rQg ʞiȌoCgUX}tdW<"d|ThY߮Lw?a;)Lyq>Mp_5US?dE90V^-K72w|-/g*+("Pr'}`)(M0y%Okj]67tObcvkur.l'PxN7MU~*8:aۿo.PYrl $<g. =ŁqCxxPs/!Z4~NN՞J`:v~ ?.qnH^gWV rqmP̑;KrKEE/zxñMR/Čܦiؖה˫5$K ׺=Tbb,5\Ř_v$h8/j߳BGܣ#O.BWɂGpOx* 6Qճ8Ms3כP9zVXu H\ꉘjrw0qKm34Ibv;X9rH\~WtP͍, 8Nۣ(+pؠ*J۟\cq 9\lݒȗ-*CG zP>?vɐP5zAeVA.,RvCi]EΤ%C6/8u%y''EJUM&ΰuuk^unbtY~A koR3I\,N"K >aaJ9 Ʌ1? %%Y%| HvU٭P:AF ֘n*  kbxյpECwf4|x#5˜J]p䩒]䊍&wD6X̋7Uqh)@sB*K~g*ǝKUȠuRs9?s·^nuw:%o ooUVt=UQ9Q4z- N ^#曊`>i!cEށVN+b|ٗK3Մ cEGX"x_N6Xo`%}4Wٻ[v>qD| .j z/CI3F!4D,bwMjlBu%!+]]caGc+k $] *:#AX.g \`e- U%-M0)q֊tDȭAI ? -q/bC& cREN A,@Uh؟Cg&֤5d]/s!'pКi F+ĺ$9Np#B0nK x]YŠ`7zX=pEvOL08΄g=(Fܨdۡ6_Lʁ/ZL?d5TKj !t\iB\>g2 dh)zه "D}ӧJf$glԏKy z!o ̙ 6w!,̃;in,G0`&NThWO}Ey3YH [;O{.Z5iUÚvz 8ok$ɶ RePk~KA@8_B!+:Z=I;{y읻?Չj>A$0AɇП 0{CNhn *4 0g\$>bXWϐutƛzӲ(rl v nqG;k~HK^)"Alg5g0?c2L.y@3_z[C}9qS:%Wv -{\R"f~1ųCwqհ唠qi+q{1ͪ~ds=mt>ԎH_m2bב#nB#y^pd42o{ e2&=k30r(OeH?`r=;WfĀlZ#pܒ}#*0i\lysGɭ'8>~5![0p >7+O\.A2{>0؍3%E [Qfs᳀>G" 9v{lT0}):5ܯFG;)T/wW'0C.~ƨ/M'<+$4[9 .v(aMNc5/S5s\^4k6٦Rў Ҷ#NCkFb0=3Y!>C9x!aqdJ-q|jCVЌ<& {-X|j7v仜\تj Q]Y,Ȑi5KnTcl9ryӴySyjD{EWO~W9 #{{ƫyH%+Q4Wϣp,A={2},ϋe3A/L#`;)EIAxb6;MaJ<+(JK*}RΎ2#~9f0d"EQs<—c 0ĪPNORA:'U*b^8tzށP$иոcoByY}SM)mۢP\hW:|!,:j,Zdx\3TM+S/h=} J~ Qp3ݷ,S=H.a:kP/I LnSE 6耐Qz^0/4fJ C^lyDP ETIugP^їH&_LgFC%"":{3 e%e8)scf+%0 c@4Ilw:ͼ1$^4)(jGw;QvG`Aܖy7K!J!vujD4 )v^iߘ,'QXm}E&nԠ>vbu1uF /eCkkl 0U]]!aYspRb;1%&sHYƸyM>wO:6&-VH P]qHa5Vڙt$Ȓz-y'G.v@g$aaSius+\ʸ:oEs= ǂA_ס&Xx X1jK]|/ 皛 R\?a>BL%7zC:&WKU&#fd OMTI5otw35-;bLދxr i,b[Ѧ.#_ 62` PFvuPn!6,1(n"D Ƶq^KlG'XZ !b9>~rK:7l5xV (R(X[9Rłr_e; F#N$΂KFt՗Ӝj<$C!CQ}dDN;/ɺDg4Fڐ!RtAV5X)j䆨Ĺ/8!{v_ csX#xSUArA% X0`N 1ZcIFph{]ZٶV>(U j@ u$7 א894`lJQ7@ƧFޓ}h_:cg&F"ȟ9A#pv] m|Os7ZO韙N7#X^:ۏ3Hȴ׎H*)ľyAߦ'+# !kBQ8'ERH b-Y?naV^)!0Dzm%TԨ6/ߗEWܞ7yPFX>61H(~`#گ:BL/_ȼ0pq^ p:xzFLfPmU_wm;-i]{cN7>PI/#DV.v:|Q;[DUVQ#EtKzT/'o32SAd> 'uD@`*T\:(ɇՀTϧ4" ׊BIMK(+/hu:scbKÛ!z }`Y@R$f (c$-N}[¶M1ThTaV {@>:Yās!$zd B(SNl4D-~Cuf>FUrˀ>xn3uPԺ$NEC{Z{4"f_ʭU, KG'f7 һ|XoG3d&͙; 0eQpU9Ӻ4Uwx5!WCpF§w?:Y{|y +v0~}Nj>O?S.}u׋i^.q1͍ 2c0~a12's\R[߼/ijJB{ܯSF_I?ϒOWN `ZBGRV$BnpX. OdpPͽ Ua?%omޜ]gĤ,`Z&mUox;yS$nդ(ϒBޑ/OC:>d fL-uAāK?3a!jc^~CX l}*3]vOprqomohwrŨ9R2n F?e sG_Uﭡ.@<_U'$Eg zٰ-# Jz)k!%{>YU]Lvt`W$HWLa|E~rl[Ă',!zi]v^M5`$ӚЪ"Git rB\`l*礖+@\# c 8V^7U$SmE\Q{cbFf՝y/@fg[?LH0p5'QO~L|eT'.<10Z#w I[. ~Hԟ$buH`k*zFnamd^J<6pn^Xmu}q8ɂ3a =cOa{}Y`I!yâAsVyH :S:nZk-1p+\*D FZ| ޙsYB>h?V}yk\ui3BRo ;W+9L{A D&1dރΟ91 ڏ&||Lx%t z;23W*ź Pќ;^:Ͼhc[3qjhaDc*> ?Kz )y=` #_zZB -B^&[L/|m,F (f3dLQ83ZowP :kRQc^3U.0 B,X{@Y#2iXA!Ò5O?ZEzyNE@d)qPR dcJW?lK<Ry,=d '{o1#XVg[e,|՞T~NIx<ք~P :=dc@`T%(Xկ2L# OB~0'èT[=PմAvfhKI\V_ =Hݿ" oZo E'`;@d;  5C=ije+ןl-êuI Ȃq^ѹ?ޣ Ck>teln[Ykd,*2ԷmwZ{e;l\m҃d?Jț | Ohc!tK0!dM@rsN"N AVHCNӼ{lgXd>wBq̵_*-"ԘÌ*daEdCP }g"n1f%ʇ7+S5H] &쌥o=r`Zݼᐧ]-2C8kH1GgۦRIek G}d4@>[m*+ 'H>N`%{ES&!4_iRf^ŸH-{^6=K=΀|L`o^\ 9 }g b͌_'I}t񅬔J@h:z02_ދ ʏۍ[+Jp ,|>O*$WND_!(֔\/4'D|hU]b#ȗӻ.n]g };yBN.P`k惙X p/A`7 ?7o)᩹֌[ Cw#{HY9Y$h"|GAT(f\{Uԑ!t"g] !TJ́Am0dv%K |%?KͺtU_XXkL"Y,u41rйl<Ȏ$]َ")%wƕ.kB298UY!namjebúxA$ ޳f8C џ̿$ yigu6ȉwίD/ <9L?r~p躥0Y改N/ix)1t .arXA9#1lqLH3ےζno%^4Nc1]!@-۬Ӱ^[b7h-tL1+5jSs2Z@KzwP~s0#mJg 8ʾTsuNZ G뤓QPmwCxC5j2qIUߡکQ 4Tu#HxgE&N;Va$oh^,C0wUExؤAzw>58Pql6%2dr&5S?GǶ>]VM ́ ӍO헥+ғ͏(\kpnݙ"DaeݯoM'v(4WSnB)'(eyWڹII<@K$툼v#_ .(|݋+U $Kh DI~|0;icMyh;jy#"ƅ]|ؘ`j5qe4c"O72Z**6=3 tqK869CBez!xGqXN]n3UhkzN#3Щsamۮq_n_m4Oj_nm/vvJ@F`_'(Pxl"; '&?Z7+crYi ~S.Kz҉Apf/bJfz,ؖ5_L3 CTٸN;24$aD1֝0!PvDN<x$v76"9/ob}CY9X@^L)Nmtd 2"617q#-|Pb'0*/tpAs&B y6npyD:ajA TƸb$zWe.EvS .x"ݏK-Ml/:PYY'c=q˲c Jad рwH"ݦZ|lFQ9h񾛪lq؉΍66 …{qyW?џw 9ͬS(Hafi_$Z:@\Ǖ_c@*>0cZ#P sH/ݑNA*rOm =4cľ} MSv@: CvBreǹadHkpVA4k*([ƗRaހvrOhBb0x'!k"=\`uG/<nGQaO4ҙ,b-Fu8QqwPj}5wvsn#A%̕_~^Pk27 I*t7lMQm}q Hy^'g-XQuZմ=@ޛ8O:?-VDž{րI#p헖c_s˿4Z!Y4ӑlOBH\çmߡ_l mQɋ [N3MCE=N}Ոӆ䕖;Wq ;FWڝX/uԃj,/+̀0yJj ^_ǣ =?㖹̥{ZtXճb!F6f͐;Nu+KC5l Rn& z,i9=y%c'q,ɞ G?6}< Rb|Q֑5`!gd|繗ou>BT.}hC@(?jsZf3r_m#ñ luiFЄE *q,ە.]$llMɊNx萣3dPs' ICwj?׃>[٢($Ea~O0CηķCik`9O+Vg)؂լbYoӱkPg#֟{U3%MMiSz&KFڶX[uԥJﶩ=&?=m=X㩹-G7V6+2X1.NeZrl%YbmG}Nofe37NWm"4~cFϭOr J Nc,fE#'VoNhcpR7Ond P},c-cގ5}BR.=8* >;~7%Q-'Y<q?1 ~&c1ƕ^pN~:݄qbuqg en;Ȓ'(:wG,2qQﲱ#/|>Ǫ>~6q]$T%VMR(oC" s}`Jiv {tdY6xI#J@x4< ra Zo`}cy߮1l,u /0/+Eg&M[]d:t{+o{4 5:]BIMD#ߖlAԾ$_zwPS%[\q564X Uv5/,VEgDU>ZCP&D<L]H#ߋIMv#q/ .W9?y􃥭4u;Fʈdґ@?"QDǎ8dM#`u'xMƊ(]+}OT!BJ&{ JfvcX L&2Yn\.~&4K%0"Z$ Wɤ2nj7]%E5J nE2dN0-VTYSԼC ᥑ#N8tq`k=2F׽}=OVFwHAňTu%,0vbP i*n{zeaDCd8;@F7$12DODUA8 r3Ha΂#S:(PqPۨSBeonܚ"MZC- PQ%%LZ螭^@yLm((3&t:ӟJFαN>Nb b { t9-v/z Ϧ]MW CDW-3lcBÀƹ{S "=s?t[]*h5 0"_y  bTc/dΧT6@tDȄ<%+PȼvV4+~enhO#0Dsp=^EFNPnX+,*,[ wev*  UJ50s6@ '1}Vr^)ѣ ۭbЦc?)dVrrhx}mlGjM=<ͽˮstQ(`" 躟"PX̯?KHRʆ %Q2@2!=WDX!2Zs 䠡&GpǠM _. 2-g^֢/5FcXLNAplF;4$  '>\lֽ*^ys(6Ai^JX~.UN\ѽůnj{->U"g\'j`e8Rw :5>!o J/JUuDFoq{d/./#*L<eP5)G!oI)Gjt;#g[2CݲL|5Ɛ0^R!B,[RIG'p;v$6¡px^ڿ-*nD4 3Z \CڥJf"Ti@XJ4WK|Ļ{fnLÛ-sjH%θ]ώ!u> ǠoY6=Ws"#7@/Uws$x!{&^xqI%&zߺأ:-~)>U=AU[zv0wBv<$ 3i/ >FL& 9@RMdHe*}*{(˾Mz<9ɩ`z4ctO@ ϿDK Ai(a9kUg/s_B~4.J-~JuurBP/'={$Hlw{k.wDr[JFO{ʕ|էᾦfDE[ܸhoYwM1yxX>N78?i\suP{uXh> x{Ѵ\0Œt{wEĖhr4d'`' N\"GC@A̝*J(̨ &\Ɵb醨-^sH?Ǽ"ZROn+ Edmv h^d?J6xyo;̗^k$ f녪gRV0?JyYмJN-9Wʱ~dp*#8q!ΏB]B<i:?Gq7v)5oGa14KbIZm0G3MpTsGjk?MD_:0=y! ELtQ΅x*!418EA ] GTmnbܭiMaZy8\0i}ORf/K=yhd{ 9m?^\ )ƚT|xպZӹ W2 ag>|]~u2Gen0ﴠ0sUĎ_nv9xk,zº^rnrt<*Ʀ$Efn&Qw5 \>.= C PU3@dzBK:$j0mʵG4n آwu(+oHSJy2ˣ? F_sjg_)T<.JtWAaꮌickD9K%l(h+|yJ-'2AP?q~0m;1ܩu;UJW[``?= $)+-YgA/*< k tRRwONᢹXq3~L -^0 %˘'x+3a`5>-yGslU\ov2RcK(Px [oEf3xNep҈Z{tVatEl%l:9h kw軥C}Gi?f;fHerGJ;+' 0ؾ,%zG8N'1|F+%<(ZA>5a"C8H1ǢZ~9V,LKǝnpy2٬(V81g}|C^) q_\ϸ64<|^^%a܀ %.cze#> rdJWx, G ᔓ5v%pUL=SlVWeٽS*DD_Ku}KVE1WFn.Jt?ŭOZV:=N\ZnTG̅D6W}7}u" fwhI)YպKP\Z4 'e|ӡq@dFW7^ڔ}U.Bn|0BrpCsWXg ǠB1 ifc1k)oO"6U1%YJRvdEQcԺ+-PQQCR|p83I̹"d}7]9J!o}Lxg/ q˿UFo|'hM6`:4;}-jS5*}gbRI4D}Y <۶ժI-'S¢hrD_D'z ڽ +hn8{L['9]!'^K6NM?{C+7!T)X/atBӸo˧EuзGsK^Ӛ3c B{xx+w51"Z 6J#N5m3y&L 3zբ~xvaii r9l#@\߅gElͅ;QryWq{hLI}W,.@}n^i=@ds ^S#O;B _ GR6I 1Jim`Z|/PŰU <^?q kdVz |$W &$KD{W w6fND=Pih|Dp}=Ak)=bl#b ?th9N)FCR j)*UP$i{6Y&&[82w̐&6>`Ahhhy+%æz ^dA/5l чXK }M+bHRըZ`Ʊn f^w20nOla­îLsJ3d:JM ;26DՆǮC9TQ&ʈ^0w{ h_"=2JD߸,,<Ҵ->=3phQȄ. x"t ^=kZm&}:[e$Ajqѝ?q+<ƲL} 3 67SG# Vx^& 3@ϟG@-9{*Ga&} gk ;DnGbB᭩~ ܉<ipPf)n)2[E=[Oa] ˰FSqg'PaCYDZΚx1k#pOilUMdX3 E1+#sl|;v4#4i8B{qD0LSR`^$¨FGmXBZ.*My]׆A|<8OWn9FR75_lH} Z;!%0߬2%`{i0Go6 6zе®?d];=%941kK´<G}iHOy$Wɀ"`"*5Ӈe-'w6""fVa~.Yo5:%p (_8M,o̕ 0ox؎X4y ΧrJ=s=\ ߼ʻn’3.kl,086K^!|NwwL)\Ņ@d,M$pP;I)Se*M @Q{x zZjɎW" eZQId&Gٌ cb쿋ډ6XiWc?$'W.LN'v8<"'M@X|^s#"r#A^ӏӮtǩsmI qPBk[Q6mMC^g :f籓N)@67Icѝ-Dn֧$E2xe*zwutw wME܋H1ރ?j OY$5f m~~ڸde-d:b ؄xR R%(Q Gj4Z醎I6A<@;ʝv't72.\;荩uWyK(5RFn6~AEkMo;[3gn[Ѐv~^Q6Q8ܵ_\?B4a\!ИCkfȚx`g <]c_wT1 XNf hX k&ԣ 8>e];k58kJ@.#n>x j8:+A+i/~ךt&4%Ix㭝cUloq Qw"5L ߴ'ֱ7RYW&\2w:3 wb3v[o0SqZE_}{q&Yp& Gl:ȍuO9ME] * Ẹ[@/%g5Fu㘃˱OW~be?]L&AG-e{eVpu^u zV+d9R6# G9y p|Fqݤ jO@y!hxxx(WdnRv(h5}Xr%qޫ<~>N^SF1э/bMP>*SCY y Cg[ܙ,=Ыxڣ?(;t,PfE*zl |{ ]AXNC^iԧU !،I @F5X,YÒ|{!=@z޷z5<_i^gq\Hp,75틂*֯AmE_5X.R=呸`B{\[&Iigo>L1a-^pz& @SHӋunwDC @$>ڼƞK-LLH0tk @cNS^㦤vGzӦw^kzӦ0'!q?ysd3vZM<730#TM@ڿ;xv74C |' 7$jd1@6n %Bf{X[`6°6h.u^${7V;' ւPrn}"V=PpFqv:q@;OȷM6 Cj2 IRC~ w>wop4ǞYkMS@^1% 7zD9:|j FxIU"1|oBjFzfo3C+{X0D9/քbE^+Zo jN͕DygG+Ω[`֦/=rx U7`&+tb$H o=Ң܎{wR<\@=@qiN6^%h[H% w/L˗v2+q{35U-ۋLɃٴS(:6URFe Æ +!!Now`ol?c"#*kjgID׻Q&áZ{̙,-gZpА˺r- H=Ҩ%Ȑk^FHA}K=j눝`aKG7c4&(iWɐacb4s1paΗ}4 _f/n.YI֐|{5: Wi焐O˕uH/[B;ˬ -+%LyyX.)t7`ռsoA.f-?E>%u'yʅhq&B%Lս0K䉖Wϯ7_/}N)PZE6) x0_ѐ(c[Ϗkydy fB[k%R< e0ag'gDSdOP آaRε@K*^$W_ rh&ۮbKB@@(~0WK6b3oҔՄx#q~W_a=[B8rz2Q,sq0pC/6c,%]@7(PB2f#6;H}=U_K$/4 ӊvo 鹭#8x@}QРiZ>ChU@~f37sړiAdJjb-ь@].z5PXWEdk+uAOVk?t%P'VMMd+bUCGa(7=!2fr!0By{dEn}*IN26!K ^~:6GJ1e1pxsI,}RrV5Lk}+X\O\wxOwvS(NPcܚ࿝tk [凂-jr~EHgBycUh&ju`B;޶#L'} qhqb#ӂ,Hv^p֛qу\Vɬ7K&P0Ј8m5h4oY~>P'*VV{+<-MrOh9;qICp9UU;ӂ>R(FG ?N>wEr9[K*F 7*Ny}⵰qnX2V#<+%u$Q N r4~yQBr=԰֌D8-()C0zUBړ`7o|Yؽ 6U}XDqF/iĐ gK7nFb&&VxHDp_(&sbb%!Mm0zYAؾdbpEWe蒲G?'YoMl 16w_'i.KPVJ=6d8 f aOXظ@")ҮWJ5]xӡIٷb¡Nr¹.z5>9Gjk`zA#1GZO+Sv;k{x&^UXɞ~Vj8q}|,2G}/k:N@ ($ۣ{ Oߒ0&<]ǓxǴ&  04ḯBA;8x],&G7h_=&/2րYUmF EM'PG=ұ5ݠڢ,9ħ0*g4[ZH&5r2lh5,wQ=c=V@\[ ]bJ y1ßg";| A+h`*^>Mqp9ԼpV5Er@[݌҈OT5NO}3M`7#(k;Qr wcGm[$J ڹ&+Ûov̀xp/.k@? :ѮjhoPG]2n bxXg0H mq_ʡKQ9V[ssSޕ:r!DO8FzY$DTHn۟޲ܚm$ siV[v Woڬw7Wu1ֻ-f JrG%hI% 6iU4*w@yxwCACl#9aaBc0|S29NܧXOǠl"$q g/% Tt(]ɗr' $%E͸UoW_OO+ы: u)Wņj5EǛZFydd@\@xX-a\<\yr\KճXp̳Ԙ!3fh]NKC7;54nUs16ڽnL*^;$hVX/x !_A[-H n OhκZW6>bB.%B] 봦E1IҐiĦX;aW̯ἔ~~T)1"hFs8T6 6*aqsu[6?r+))& 0D{o$ (t]q0G4MpS]djv% \z}gݳa~h6x1D #Wg*SrكʧՇF@4G5H/񧹰S0D=8g*(ǣ#U.Av<*7m"A\H.WB[l! $}ڻq}YY<H.Հ)Os=2|]ۀj;g''w9sShMQ餀h\_kl9GU֦ awiݸnr_d,}Ӭ<8- Y.zU2wr @tb6[ͲVÌwh^}YS&h`Y~操tqqPwv KeL XH8U ],v4ג>=h;Q9/>rYIi;(y2*aU@zaN>%UBJ[K.E%➓dWzgQhbt )veyvo\Iaի#sg9);Ӹr G}O>^,TT},gwJ%+MuK@Taǿ-@]Ԙ?rSup~WT^A459$bem"ޑ %}:=B8ә 6fʗ;1dgwl25tA\=GG(r*vBNKI\5 -Y\^cڞ)ytn3.@PI" Dw&Dɿf)yФѿM'DHk顺9E TPnc_ 7h"?>Z6[\!Z:oPès)PaWntKf!i?hE+u_b!gbB'.V_~'>pc|c'En*>`Cu?8ŶZTƲj1ky-!G@;:(+Y#N:.vuBzHk_w`Eؼ"eK};H7z T<2ܮǁ#MPҧ7b|d.~@iiQC|Uh^f"X5ݸ2`ފ @tŦKu`+MO4CW[b4O] ŭ--hH9Ǝ k~@=4I` lhUkpLD=v%dhyY4!.A,83Y28laC`/J P:1Y ,]yIHX֋eu [wjbljlV 5KmIj˒ QJ=wqAzT]LX㎲W0q/0j"H`Y\L+_99U]ÓፐkNYuVvލVHFQlQ8ddk`ɏ04f& §0&4/ySpRX#[S|.j `gRIqL. j\df2qkF Chy#t]Y=/§U:ˬ-}B(ޱӤ>k=: ᳓ `DŽ#@ܩcUAqnu7C+wCG#1zyXTB"o?hcQ4Cxizގ5LhX(̿ pW+c}a0 H(%U4Ub(0b f,eN,es !̢T2^\ra5ᱵf@AWJm* U;l4o`2ޑ=P+^V=ӕk!iwiW Z8B?ir3q6RJ|I 5 J0$ݞ=bEIv!S˽wW>qo`L .Q"K>[MΟ"U9{2k+J]^LŸԘfsqyU%Ic1AOCV2)O.nRI^/+} &3tD l5qzpyBʮ5*cRM _m J 'Xi3dӿd gV}i}ƈ]ޅq%$ۡ2XlԴ?[idD#syG!-H^Yjȯ35?T j1ӳ]+o$ y\!TEk%uW"cEynI4#SCmӈ^OB%%=J?DNqT=<da&K $$zz U#&23ZyȪ/Ux[>XqN/흾S"5kij?bۇCEH dÌBku 8 :R? %O"G7̓sXY=*]g#$$JL-AqsÛo:\W$Q^tu=nn61N }q,>/-ዐfs!kNhї̜Qea+.B I%z6ӓ 0KT/ 9<c_MʅWLՀB#?sW` y*vVtŽzWX@z1Ò8yLS=!A0BjܹuEs+GDWSZĿm|86TV2'>y@9[>G' % "RqL:؞Ag@~cF{vBi]mc* ;T,XLD`^@Υ~?cP^v-bX# mBsD%9 X>P~6h4,-|(x-}Gvh|r ɩT\ i./nmLP9OOi01D0|{XC:fP#~Qie'K3w[TvY4o:?x7i -OA}OM : !H|W4{`G댂7c'g7$hɑfʍ<w- TjՐjd)'CpiMݗތ"$QX9Vjm؄R;wI(#3jH=7hu_yZDmRo[@u;L**~'uP ,{8.xw@ egGL{hTd2磾+ q{ YˣaBv XhIZPP %P (?@?r}=~,J{.Aä<^I+#d#zO bNHdH2f~%]=9,;%Mb7Oko-Z|>˰ۼǾ߽ +@jqMu m-Cd6ud2 k\ ;"B:@q+yCeKjVNq ЁS;g䨦`(xb U^wJyvtq遶8%yr΋MacW 0I!B鳅)P \]N9t+"ӹ@` %7{5^yw=pF׃{`o/ |J›ޠAhv8!d$v9&eyX(>BCw<ڧT^e{ \ v̹ZAU2ctvIP~vei dQCoXpH3/+[XakG|XX=SLݗKTTkFZw⁊UQ^ $n:')Nճ.GҪ+-af ]l㾝653wG~(4 `A%Yxa<hˊl rM'wu8-ܒg5z0tiq*(AN* p+IX'}V{UO]6UKEeUD %FrJ Fn>]|D Sb &1b+a%+?bNOw)iW`x֎QX!>q8 ?˙<,ÝDS)0NfRQGMUPS 10! GF˻3zuxWRRbho55@ɫx2iu.pHˡ}z nVxYoDs_gplZ4_XKM[WmOxmt+QLrmZ|M!qǠ R.R<@W|wkI;I'3#RSPwژd6K`b2:pCG]XQ>:;WJ (5k>\^$}=Z58NG{Jݘ ڼڤt:a3 /B{S2n W|݂hZjd*KAy$9*N`X4q '\ ;n=)n^36֒u{u1rR}GY\仝b\k0 nLm0z ;Nht+{Bfqu3Sp(}gNy)(U!A+N.`^ 鉽ÕJ$Ķ BAf1l7GʻxɻЭEUV*ﲒ={ZLo=@=6I;|u-EÃɁ kuy,8irF EZ(yk)qo2B]jT&v8) -;QaQKлϦ|Cw|1\FyZU/^=~xZF axFz~`&b6ĦGm 7k7Ts|&J A &:v bͱEryNvJJ]%2Nn,*z ^;|ydwyRw#vgG䏮թtB&=bn.ܰ!o ڵu-qE׀}x+ ϗK$I57_s7zLJ)C{5K`鱮SXСY9;zNU*ч~ yBȉ@7it[m5y4$σ7XiegVeΤ$5O1c'[#SUm"=>5V颛n\V$ lk1 !/ЭwXe$g ̒ !kx8-("pqyK]<~Q8/|$m" 1SGKv~!Ml7%!O!:q3:<2mxm'.y-[Caȿ)4fy1puT.בּy h D&lqF6BӃ{YuGũT2&=Jy:4{ ~v#RN7wli3_\[7:];uX"B~X(W>M8+r}VxyZ}E"v,3Cy?xe]ٶ;cӊUkܗOX>򄃣yՀ p׹vEzu ۙ[ntIoc(Be E+ ~XEm): LTG΍{8Yp#π\L6afQ9Kn;Y(-3,ZۖQdBǘAɒ|pa R^qj @f-Gz|{ifƦzp'm`mweUmH~J{`fkŃ9#'1 |cc:=KZ^qto3Il u*̈́'54tVz+ԃ:cy*UXۃϿ; !žeJZ'nzL荶6)\NݑN~;iE|c<@0$oMi˘xTf&q+m6wR~\O `< avϘ؛3^aqYwCdOO1*C/' O g+P7ͳ%2>9.%b%k6Ub' m'{>N]pu*7ΔPˮc 3iQ q\᭬1Ex8HWVΥeKB8ur5umel9 - /D]C3z8>ٷFcƼI$[s׺FeOhfz?[:c_Tq. )'xN̝*zF_"?$Tm5rA DkCNxBys Ux1/Ki]J?4 Oդ߃c^qӁ;w~9aŹ՝ةNFfFV"0_@Hvf.NOaD_! N-O=n7"wt"a  @Ű>)vw.D6a Y;Ja{ɘU{ݵgY@S_r ƃ6pAxd 8O "HX'r9== =0#Z* ֐l{HVm5@X]A0zF.T(O MoBB  QE8KSc3)R^|ш9&dn_K׈Cvd)Iu.iCxJ%')?}$o_O!}3S}G󯾤gR`C=^tzaA>5 Tf_Գ{E_ok_ꕿo'[}H?/f}լD|/E8t0'el]*ωia ̯̂ivx֍u.`M%/iѱ6݆XS sHd=MfQyRno@ %}1Բ+{Ju9bq) Ok&)*[4hKqJi[ow)OIh3 aL /C_0& 0rHZ챘(q|ѠbXTil%ZNZ=2\i>%jR3kb4rpp\sMSEw~q6&H,><{+ީʹ?F3[:]Dvy'ABۗпZyґ+Jd3q6b[;s)ouAGTo`@no w[u0Z|D/vBKs574>FF)h/aU"Cĭ'yN] D?RB3_ʋe.@UwKX5B8ߔu]];^XړcωsB[!Nj9LFu+~9{ɑY\M3x~{yS5VF8( ,29wǠ]ʉZ.727m5 +ezֱcgq )?fd59xo6Ohh;%A>^:ԭm#(Zx=<;TCwvc_(QWD$.x=Sh\6Kʅкx$jv=.i։EdKAZ.RNoiڻ$DThf Y@ժVġ4E#Qn 4nt(d7~ 7c(.o PaԪ{M5u_ŕAm@F>>2pw@S7<Q ?vfxf%M48hqY)q>T&>2Ӊ ?uŸ ҃ Fkҷ&pU$j5uJa+_ɠ )NCm6ԃt z CiȂ{oEJ̡͊(Hx?J[ja;Q׋ o!k6S?1}mcI\fj7U=ɷN:(oJGfݵ[ =qtVChu3 C]4zJdUf<_Z炤t?sԠz87%K "4dU UmmCM~Ј9~3AYh>:uU:<8rZkp0Ymx0"@EXuka4"#p4NjZMFapixadSpy]u|{HppB_ - _Ŏ#y03myc2K赡VhO@-`yݵ>25@xݙBj?H:v$T26d)C/ISH~iR5s&uS͕{c?Z"Tfu da0o$u_/>x HV76<5l2](Oj~9=wVʔwg]?i"8qhG#q4=z058NP;)} 1'7/~t~a]>CoZi$룯QX/%^-VtϾ/Y9# Wz{؄⠐duvAV?M:^Hh:\%ysfG v<\X9&#N*%檲Ҍb~5>|V!f&`K+:,69lq&ȵ{8v0/mq!aO .YFqRq''ܫ K8GAfۯ̎x$,9W*XheLP0b)Ig] 1#SE8;g/E:ˢ7@cENGS (y: l4X tAϨ+(6Lj4xtTR1}>Vyd%ؓSKp`KZ|"U٤ fiU1X)Q[D"asς;AUB˵9}VKa0IjcCt }PvFȠo^‹_54Dk ȅw&AHQbQrdp}m5l><)y DyY]U@yj U#R,߅(͋KdSs- hM6_̔kKrФ3}ļ/f8`1Hh2*oE"-&ઓ: |- C~pfn +9@<ĐhYJfugQW{oth:#A};(+ܳ?}/ Ru;wQ6rȇo}oa1Ģgn1UqЎ09XcB;D ӳb uyҍI폈31~ZO@NxqV`M!R"z\9Q?64ZK+AK~I >\j4 -Pd{ˏti;*zCyZ`K5է*[ۍv:9)>Ƅ m*+ыN+!*afAR l MyP Xhzñ ~i8 so@߸İQ{<;lfv030 :d;wD`$~v!*<ʂ33P6K r`ᆈsIkʣ<).zmz:SGfh%?۴xjFdxnsj3=\5=701xeOF=k3mJ0#hV,FO6O O/ae͕oHFQPfirVW q}'oߢBP$O@" qZ KH)OD2 ;v1d`H@+ƮBtY3 Sl%6zʏ[1DH:ŸI&gGQy1=+j4a;&nJnxj=jp[v̾ó JK;9Qh=F|e!OsnqXn?g5MFKc/!D-FjX X8ƁB,jƅS:~e :|kʦb_`NFW8љD< 4dlsS4"`/"S˪sW|UDd12 <7uMRrSy׳.P4E(U[}.S rbo7.J mR8)UGc{` O[̨ ۫j?-6^"d$ Y(ŗGKdu2sUy3Ozg茽i7\]d̽䙔@mk!H⩦Ra2A@0]T<]ށ,ڬ|@^$3|4KhMdEvK[9;:F!!."S!NM#gt߽s7{>%).u,98x N(p.h&q~KꬴoW4-3 򇁋XeH2X l6u,tmf9~kw{ፗVvU:@ ,~vwP:X)nb^WTw _%uKW1YYV}ZD(m"m}+[z }ÀuLsu"Btнϫ[5Ծ$6oZ2N=009RrFY[z:^D0cȳYT*\T ϓP8deJbϗnp,m(58/ ]wnC{Q%X ܘ*R39WM{?AlƳ/ٔV>^|F` P[%j 9#4~̤B&C!`pC͗{{Vp8TRQ7v%Dnrf2J-~F[ V*ixQfr&ݲۑn nd^,H}L @"IAV5oG[+gw #;ڰ=HXRP')ܫN]b.PKp8IS܊d p?Jq01[*5oz5d| C"gbHdiʿCU$7 U۶/"ծbQ|:JꎎBML^\'¥,k7F~$Tށb$mze;r97@!|||!DNu2 2l~M`.@ᰈ0CFݓB_4扆o}/s%]^Yn:FaԓH~ͧ,ij"n.ꭉ݇3m02nbp nxJxάi%V Km8fF`4>^M9D4/7^yڤnǭ-׈A J;T7b Zpkn8h~1oT͖8\?, ]Qm&[JstM\Do5v_#@A`*HU~#!"mJH7&n N@yEq/o`'L֗jK$w2zkjV #^D l&k^._?&ZN_]xFJ^Fb2O ^_i9M^96- r|I3z44srǫOoҝTD=ǝbky{eI+ca2!'#[Pc dk3_*Gѿ:ӵ 7ȏ25~Ǡ1Р _[?c-pxn='9qkҹ2J|rCݍuC0O([Mc)5Q/ Qa agyRO]ةŋsR?Ic+ubB~t)gx*SPw+ $@뙷‚HW OM TʔK'!0LtPG ‰ӖVly?Akf^V,H>HFӔ'~+=^?Ў4Fl>s iN&d8ìKٷ֘OVVn ͥNr*,YjR/ʹ2:l8d;ev%$Ct]ps|9lipf+7 #s t0Dd*smiw᦬^7rgdiZϖa#w3.dˬS,g"q$p0q.{5/ ]Pܝ>Yi!ZԈC,Y=iM>z~%M6.C_'uEQ6HP!˽NR c٦4Vh=p|Y3j!0m|Xn;!ettm'E.+W BMpjZԐ,l]s#LPrN`y|nv%4uS9uɻe>J렋Y(7e @eu# giVtb}~P W֦1`D"?e/vtPs;xT Y:O> oz _9 #JbZIRYد2b!`bjsIޡMpb]%Gn=>K)S!tPT08c"\Fn((O$uikAu'/Ni\Wf:A3o@F)i. f@qF.?d(2u"[aRPWBa/[䚥x{(5J* b$x4#b YAɍ iDUL}fE_4YeNU&j֏ rk^gYiϾJ )] ͈6;U?ݖOBC1u b8BWTr 5N3f{@_6bP"9x'oQ>hR 'pdWoMu!{(X鶜I5B"fw܈H8k?c166 $} WRn3ш_x"dKމ~4 w_6>*.%G%+'m0_ xഭf5N,ƓFXh (nDLBt| ƟrcVj=V7pgGɼ4/2} 5Z3,MtozFv-^2MBjCڅ#\:s40.|a B P¡w GQ_)v1lpSZ`$Rm+c%$Uf\cj$Kn$T@iv.fv楡~rޮ_q\$a"\ǤoēY&$2~ x!A9H=T^8<,p03.w;t}fKa6'\+GI& cνVxx$4K g8-Infa6fT[LQ՗/.0}ncB RAYesl<(۫K2>U(7U)RtOAPiPnnd K>DֱuF@W%fZM:'oa@< jIw%ѝ7n-F z7k/sAY9G'&rkc SĈon'\ܙJĖ y#wVe ̒s=p; ʥ[Z>qYhv K(|T:bJpr+^0sA/ xpP^?QPUzz< O".౤+>ar!@mOK6#0zsm%9S. GQwty$SR>\>4DhUgA.;/$O-\ɽR#ޒ Q)\A4]HRA[kC"(UsE}==95lDhw)k܏ln8]L<Sw:M^ML.bU#z- OƦJO7?Zi6WI=PidW0AbQZfAތ ck}_H*Dv.bb\6/9ƨ4ƦsRmlSZ8j®>uwN~#%&ǢR $rԩ"* S9Q fBldVdI]~k65[6"" SIkg:Knz7 C24& !\dӜ,P #iDmi"*.{}'êq>.{r bXvk!v|Pd.vY@ a0uNknB7T~JAOjaGM *| Џ |`#͛ f&wa,?2 ^V'M@Kx.DQz^f oxrbIFjhSSlXeDmy'x}ڰte@1eOƶ}Kϊ&!?IIY#DכDw )'YHih(p5$14BCF/+|  cEM ' {UBu$ʢ3!`[j?9ؙ x#aԣSbd4.v:Q.WY "H݅5s{>%1+JIO-o"18ѦGx&ffЖqr'_Ql 75yݽ`Gz+tUq2zo"ۊ UE: Q=Ml6;|C ñخu}D`F Aqn)Ӟ8MeQ !ct4u#υR[4N˺~빟 L9I'H-O Wz$|Z"P,ŰNcfBUQX44h}yPګH!uл|uiJ &S&)bS7"S)eip0hœܿm: cj[|(gM$z"نY9`MaXP^}l9j&yZ-P O|*%P<̼Lw\B,|YA+H甛wƀRwJ~k}F?TʼIJ(dfK\AHabWN4Cz"F KN$]} #[Hm{Me(Īy(`;j:p!/1Q[U>>~6r#<赯#/<k;eMaY<+uM:MkDmO$@pWw;9_/Nۘpps/p(O0bXaM)q,nwx\GAq+Yef!liݞ'8p)Qk,`We0,m6ZD$NO|OBA,^^T~8Dz9(BU5@3,W_RHpOnfo ^:YpR/DsbkS:}ts^?GPІty:ӟΆx&4º9B P$ %>$B޴cXf6uBC0YQ:K?u4H>z7+tћb(15DKiADs.usE^4I"Oi~NMX" %a# ?# _;KSw8Atľҋ ȴ-94C'0TAɿ|0䋄wbh"Dgx0 cxI QTogљkS֕yCfC7mA8C,U*¿glM]rQ}-2tPЯO&mcy^vb^їL@麴H3N  O ؜Z7TwYtU#|&EǓ[GEN0ym :HAƧ+t98-:ꑓna5akh8#1Vc(TPҲK699m{:tFAT.m}Toë lsi@-sǔbݧ`88׎q` ,PzC;( Z O;Lo1 u؇he `g#≬nPrN?X#ڬq\%DZ|"%@dFk]8h8:*vj!Q;EYN=3,A\PlZF(! *!;U ̺x]|0T!](d_2 *lFkn4>KXTz9h Ok̖[¤+b7rbXƯzlGFp@kfI0[.+TugG!ji:Z[3+8Gak铕'%TtZS$C["TSe5 |`R} p'Ջw1+f#ؿMLk_2?*7EFI-wK' \%j@iU l3\f&L ;p(?QǘA7:4lDWK95;=d3&ڐ1Yo;a2D>>RR1 $7Vrӱ>zMm_t3!=!իhRتLdRD^"Fs97R; c J/ \K^n,vrQ=+D UhgYM$Ι!:b QK{8 pd0ĉheU)Z dq eo B/X`@OJm\TE>\!5ԗu^CiX_Ē;L ^Z[ LRMsdX|;bwmn\<vL:K$u`tvn߻t"*;Qʼnh!o2)́#{M8:ClY9mJͪΗE,"@b ~ ͠001x*C#nT7zОLQ$ܪ> t0_W7v:[fTPQZeڦi -]p洼N6@J6cl}0a (0+ڤaXx% %Z̖((}߹^E츱 RgZR ,Fѫ=SO7:ޯ)4S8* " r˔=3解-'#&{[(OgG/٩9U*B mڮnkplt)uKFy ޲9?~*i+ef})㖿 RaG iެ@GeA춘p8klDh/MSpaж[GDž/%|MD79}9=,؂9 "jWvJA!nӜ|)`ڋ:%ƙ.8 +g7_,_[ڳ@_WhrlI𽞇jNOZu r$F4)\r :58ݺ%%.qcs ؠž O"]uckZ`YpCْ;휢D R\ & ;2WcLYK2ɯT>,տ!))CΕUVٕ^C>g'D3$8\$Yё],N}yvX%שt}Mv(%,ۇa5rS٘xmtr/бGZ v~ppBopfu =ۈe51=X mb{׽ڬ?ɩ}) fn: [Aψ@E3Z`E'uܨ^䎌+i @]G\x[xoL75-0npl,fJiy@v"Efs%|?.#[S'$T4S_ϖQ@\srxej`}%0_=]܋BnxcRi>̉R/9*%kPz?TAGuğ1?5.WZ~V9+?+M 7cB 9m'eY"z.%ոܑfpyp.omrHIP$ePgG5 Hwt;h7y1U |ӑ<6'Lx[nnyZ~,*516kօRUx~⊲K8&7S^-iOtb}4|JHlq@lle'PIup?!.wf a"9\t U)J9Zj>?@"p}u sFUK裀kPۛ@al& bQ0wJ@5J`$ߒ0`y@bUL?Y`y*h}MD[)\ %ڕ,lߤtk[[TV0cc Cv=sr՜A b2{ք)v9ޜSbb~ ­(fqf>Bz+GoHO,=ܮL·ȸZy 8q-çp`mc-Q$%PK  j4E&N1:V"9z7w,柔M?sd&-ND&u˖$<͢*~E ֟Q58>ݙfrOpdK6&#XYe4VWML%Q G +`ˆTI bOzEΟL[sgwɎ3q<Tsb.2y5XV`@l>SS6Vp5Ͳ:t2&6 $%t*RtžwqF8ѡ)G&R6 -tA/հgQbKPKδEgNER\\DtuCgPj(٥=+ q\SѺҰxZ|^ŦP0ӱ\,/bZ_E_ 6 $k zGՃ.kgvV!pPo/їy2XLf˩,cqLiMU0^v~ؚ_U9#f ,V%AhT ݴ#7F~<7bň2_-3J kVarJ8tun J26wp {W y0U4'Db`_mk_yu;wr_x yGcکީk1D6T ϓ0e;8u}TմrEyoPjY+ȁUFf"7n#؅9̖ @G$WN(@$",bzY2ZE}k"y?d:`wCªtp.$*ޢД;Z@ {71\7.0|U_g;S uKiU/S>g60o8 A\6?$%AtJVvZ J=Y.fh }3[qoQӢUn |>y#? ܦ\@ּEN%VGȄ f0&sx|;\|wRzࠝnòҗ4þtSX>r@s%zUJG"xMlqkGa@Z #w6ă%⯎[%tN]xt ;Ϭ:;4#:IxsN:)AS9NH#[U/6H q1C#LX*K%| _Va-{'oI6Χ4Szol97~GyN i5 |=Q `Wvr邔krG_f$AaPdM KzcIg|똎m>`!-fZ)2Ef/OjVCM/p+K-y;lȃR23 i):S}]B{vaY@4)Լn &K0ab݀ǾzCdm\] 6+>frk}55zcC@)؍Nlt ORuKl 1=&)ߍmݾsP_ k  0t kg5>GiG&JJ2Yѿ %\d?Y+N6Rj\ Jy렛BQlx̬Yqߛ^׸M`O w.CоrFT?oQ֡'Uv0Qv6*#TV{W$E>I!5'B:HPxtRCn>AeÞ-K6y$sb mm; q膦h,Ig^j2WF@uʒhzze QvƵG0+ FZkNl ΤQL'c";NN>B tU_{ d¶` XF~7d'4]$J?i+O|&Z&A4&q#˘SiH^(vsg}-m q[4"=<7w^Z„KI,.qȻKPpv/+$*Z)S>; \W(wΖ:ګun X ب;؍ D΋ Fcp+khaB)j+θKɄ̙+\E}כlo:jt.u:7O4רݫP~|I*~&Dtk 9q,qW#ƕF9Z:Ky&:s`,ü"}tˎT+/R #B#oݤ4$1R™b=x׋P }kw:*ߛRRuObxQ oq&qt|,(sCz5q%wԩ9gY5Qnj?\P?c biLNK@\4л惡M2;z- 2 -݈kQ!5,<98CwT%MXStVZ9d2VW<%pHb(Wl$zF*FtaV"k(dы&IlZʯw-{E _KdPj ֱ-ͮtJ>eoInxDK .zu.iQaR٫ ͚ vO>.?]=Pݰ^A /w11P`DR(u{db΄&闋@gđ'?UYÀZvLIq FeZ囒*jk4[,2ۜ,5"dy~YOЍUeҽNRN@pҬ@KH(ՍFf-œB>ju(7'αϮEĀ/:_p :j<}nQ KAARyg;?Ln#8m89l;C} {h"@J=J T#2[BYc^ug)vkl4 o D)3Vt~$ /%&GfJ/啀v'hgX&sYdwdȭSrx|P>ѼNj&Q|xZSQlo<t1XKr t duKvm6ilh Ll˻GsrߌQ]RvT~h]^%cuͼJ*({D[Efj /?7BF|tp3Pxws턑0B ')f1k܋{"[_yk;$!o5PO:}d~bcŦx#xտdb_~dn@ J9Ȑ B/btHI'L˗dV{Ԥ+YTA$ݥO?JbmYV“y׋'kۭu8e\kDvձ 2&*tuߚeAus j/:^U,Dw/WVV֧_ƛ]N#ނj=R#} z=1۬isG~Xծiդ! +h|q;ǡap@԰~]4wb_%n_P.4i=^2j؇}EDAٶo y^oH}| lM+Q{.uޮmfyVzz:`< ߣ$6IFrQo``W~m,ʘդwIݬyVvBЭTX ?\9fq) ~:R0o:IR*(Q81([|hmV JFAO)K>آP+i`Gw>5gy$ 3 D!"x IE` }7cr>LMY\>P=9*Cz_wf rdtU xП[WK_+lX@$"K]zJw]]_<6o֯ ͙ 꿼 _GpQRs $LrDZKNԅn3ڬ !Y)5PRu:¸OF}#kyޟ' %gT=as8e-4SwghA's-D``A1ngQ;ڸqRF5?jc  &lע}St5%R2Y^᧲ oIL~H`h&scf=.3Q./?WT7Ybm7NʘQk)i_t[zo,4 ՁjHyyYFH5& wCf]U6œFrfXe?~0bѥ<Xʳz,M6'x;D%^N#5vr<2F5(z}?6h+_Gd4c:TEA628?ѱaC! 7Nj b᥀ )5nkn'pg.㇬w$h6ғpI->xIH8aRx53BR_tgCp>{5iz3tc=xh]c6ouSWEO%ԋW6/X |=Ӣ|eEy&-)NZ3W2H 3p+REvCq1y;p!EJH EzΕE6* z؈O);ѩ-Hp|hB芯w]w1(Mф [oI+SU`ڮ0IkO!ԡs\h3,TX0zιM6T,_Mz"%6g4ǝUPa}|PD;)?6<1p `zrq`QЕ 5ٶmL "ňP 6D*XD2MK G"SIA1kirw @QTLS~a 4¼W=k&I8N2@_ ^i7<tJzlwAIѵt5`fAu?B|Obx! G) Pi91N ?G{4-bj:t ]\ґP]kGX{'b;iڌLkDui shD!`=|CgBE:CV({G֢BmMyJYz/Q~9]/-<0<+,:Hi芄1]I~ӇtSZqbX|' 1m=7 WM6aJQCsHt?OKHVԀ~R9wύK\=?âcɭ'm?="A'ZSaO[3 呈Jzh0iG4ego mVUUv8J:"KMYHD~dƏ/9KX~fLmooCKLX"wWı|4S֭xWC$ϝ&`5N"#!(1%<|)Z%^wcB"߯?#O64̀ ڟZm%K# W&H9 Ā;S^)⩧hL]';g U,@>*MWHF=S%aYCM\M[5!N,"}/EM904ro,N]~3ԜmAUH9yoH3tec:NMd9l̖k] Nk%t;܆-vxIg *hDuͦ 0~ΚOswn@Hi08Ag}f |K=N#)K)EL^yʾƤSz!MgH‰ F&o?) ]Bc=x@ȒPF"|lFίS?@W_܆+^|:64 ȣD72 RBeI~=2٥c]V<_(EuKi;}8re .+Z9qjpݺb| swXQEb/MNj$5Z"WEɒN&24R&v[p?eVwg^#־~%bs [ZtW!EJOor٭j?R~RvT Dc]eR eIQRM<_WQۊ(]SeSDc3l].2qe)_=Jюmjsק,aҼGQoG#Bm9$ 6.U Q;<һx{Va :<vS>~tU %`. |6Û,6cly( Bf]ѫre=yH/|QaEJ+s 6~}`` 3{&0ӦK[tڤlf1[ ʕ*PhO=w/0(Ec;La(ts%L$`b* ;9=|cL1Wبx@1KkK#n~4dsC{q- [%|S ɡ"TDJM/˹N0@{[GA47)t;0jTmF|(粲9, ZgɛDy;RO|\/U 4;5 y .j]"^1ĺ>!6ׇܻl2&yH]vjZWEUš7'̫eù742uOˀ9"wExĈC:DEYWb3c[,k▎-(}!4(qx Xǧ~ F=cu>q#>HH6Sޜ[ΦmK  t̒~>ifbNjڵ JȍJ+Uqҳ\7ÀإzЄ5>)ٸUVaj1 Fr2}&hĺl-v%?8JBNDoo&ѳڔA5NLR)eF$pƌ"sɞ-hSVNNc{ .wVnՑ rp^=㌫~Sp mZ;zDH Z}Q3,Ȩ Wڊ-']hXo%z:k@Z-Fu^ܩ[<v1[J:rϙineH Ȣ?wmUX̉ynHs"v`d]rW-5ɮ,c JsṶ.'"` 7b#>N3&K޵[mc4O?ys<n]\8N/ɑ3̪PTQYnYQꖺY s5K WkU)IHoFxT*9utSj4 jj~#BA>~!˳qѯMe.BHֱ視!Yc6dce!6b1PP f\-gW+ڈ31f6 }0`OK5fAqNA8.C""T q),EzdJ3-;ܾ>J)Egvy(?v%c'4V|}_Dh4-C_x?q w 쵖J UfaBq+ZTjƘyd"^W 0L%"&QX}rUŨ!g܃\- B,K?ٳZ% )Y1+L Hf=˕p-Q|A1+n8^M#I|QlJ~3 {N-|QrJmx[r&'l` 'dL`D gG򾥡+t pϼW$a\F4_\ ewa,%($_~9) /{Yިq_M3b}wL~'CrB ˦dEpB+[d/3$g ޙ\#pؗtU{M-P5weng 8~lUtTL㏸!#|bK[5gKlYW0<[%DW{L+`Rn@ `^';> ;@_/4>Q&M/44s0UV #1< tucb=8 nokd&5|84?It@:P({/H0+Hp p\7MIS Cŗ o<b̀1Oogjڂ &^3NNڔCпPT eW(&%nȖAa@|.%/1+%=Zm 'vķ?yDxO75j1P e{O5\m1[~Iӗ"z?%k%x W9"\#(vx_j<d{yǼ?%TFLߤȇ^Ӛq?_ hPȊ]sŭwAi Ʊq~hz1YV;P3V+i)tc9!ImQBpc)VLVW McO/U;S%FlàbGZ7#4m Rh„Q']"g%$b'6D6MXn"r3529e1Dk7Sq2]0zPE:o~sS .ꋡ5QNR."+)7N, >{XL+xNR*ǖl UׇTo`FJՔb0J؊Wu gm [Yl[O玗&~ F!&< GzV#KE"/*B!܆L;d/wKL9XLa}b3b~wmDkۿ+ӳ$v(aǎtC?rx6-[%VXo3c]yLzcAtQ^~0\VG[9)Vvye.k)/$o<謩D>U8PH2$6G 3qyooClRIu&&j3*#[ 㪼'G ſ6Gݡn{:]]G|ߠr}."#1HOb]Ma['JC4(~p)2 $lAGKl]RAo"FmF u ?fcT]r=q9\4 ^i~$ RVI}?}0ڼ*ud6W!Ę|{Fk}$s1G4g:79.oI[k&w8ԁtYz"|8827'rXUT/+J;j? Q`$d8 } e Xk V- bfN>e`I<MZJmi ~ِ֮hZ|*C9DG86:77-!䒠OφE SjGe>"|~P! ۘV)$0Xߺ PíwwLeH,@2ؑ%xG5؆PUPl ӎ'wMI/jU%SlXa]iڻy DQ 6"CZdY^~hGWt QzBuykT08= KarD-S2ok86B gA'ǣ˘C>/L*vj‹oK~7;xFx9FQ?Xw۬|z^ skH((|k36u_&7%yhm\Wq:WD'kpz@:^.!>Sp(;_i"Bꫥ=672z܃E,l0 s„ȚYLMoؽj˔41}kuwVb$($IaZ̸u5򪂰/|˯l/ 51(ZW jLh5{X]Rԝ+uïKXj,kicddL=T1`u.M^"~SEKGZ^t@-RuB9-ey BOB7LYo87b: /je'9[s#y^.\`ډk'Mu~%Z+ nz v^uykFgRlrfH!~45šg?/- O 'v@#-%YnT:'_Ȱ-xG5# xJ1R-:i"Z។ّK!$͡l;_:]Dn2-9qiqY3z]*4ޯKI1%ُyMaø 8o!'jj0@y5!Lx&?#תe$BI9uDP]UG/e|>Qp<{-t/ :zE֯ !;,΁߫C<|^2ә4SyWbWy:htul-}el#Ml mlbĜiI(/JM$h8¸&ڵY3co]ip9C:釆 _ :qOr=IxG"SlrOP@l!jč?<80OV ~lǖaE6O:FX AXkЬaRm(pPKC$M S E`a>'zP?+zU|m\uXR2_ `OѨzJʸmS3{͂neoBJ_"l݈z,舍ky++~qf<&,VDkp|N)1V(TӉ)y, [LOIpfD9nj K/!~E`V5)@]B.浆XyMs4f9`<<n'P̴a(# @`VNW$lbRݒzoE&Y}]\b@V^|!fF+ o: wl9@T|W6U \7zG:}WnOMMTYyC6~,V5,P a5 8:as5p'1Oʭ`4Ѧmʗ5].S97_F'lm#m ƍeh'iCS:ؠM$ =8`qzd [gszs'^}${IvL-CcXB$>/9qX{Ǖ̲ {&*ᰣeݞ4Y:j+Mg.c1m|vl5 Fn Mmk<m|-Љ&a+E\ɘh%IH`E^ &<8bPI2^{ YQD{.\( .6 /G .H@CS Т.Ct2IVr-'6ۨ0 uN<_wK6!5}h}UOOZlr˛p5ޝs@0;2Zs'y2Ji8s 55ntD%0* >b:i)TGjd3-zW@ӛj_mF Q ~ϕvH ߦvi)r]A7?ra_2 cf?鰢xڻ連{Yvl m nl䄡j+b9YYH"4s$6Bd0;Ssrj?=l5-% hOZts 4b~j0$҉b{Sޫ.\'! ҷKט!(JsD{"*}ֻʋ8lMNP/C> ѩ2 8}]@ E&;DDЩkdn5}Gs_NtEȨ pWu: N#?KyͰ$! !3>ȕ6= }*Zz|,-^a4}t/۩I d:,@ʡI,Y4bԧ3m1/[2lsQϖžDۇbM/߯fiCQZbGN|0_D鰌WqC2d(թkz!:F:?uLGCIAxH%+Jwa/jf.]o3N#b2Gcbfer4Ȑx6 8,txC}BF.ZM?9>f^fߢNڠo;p1 ~8 'Hsslڪ`?fQ6.sbF"CzP|{ɳ_Ǝ71P̶ "]1-)etc9 GD5 Ap6Pn)NIRDKk 4˦Y?кYmw,-ְ69^Hީ2NӉq-|\ Idn{lbӮ/[T_>HzXAO|У"'*etl^;!*|Χ&t٭ C*IɮrVUS~#nW^pg aaTKsbRsHLcr"ac#jSxcܒ.ʃrׄZ H_ti:3ᵑ,u_Kd-sR,0%+1Ex $nꒃk'ni4="y&4c;O-A'Z4+6_ԗ+GAKD[ VY%tbs\@fn؛ G2cO+h #:ƤL RYsPBE!pORߡ̈́NJNpڥux p)28WPf#Ѩ8%>fl~$1@BDm!t 2a͂fG%v\5^xyUT#ؤ7M显70mն)\06\-s} tc {;x媝~$>LQ:l!qz F!I/X#8Iqh3hu<=.F`vfArRzu!؉ JA a n>UsYTA{l$(#3Ⱦcn\Bs<(#7ҵi("iJt_| ʶG/ʧ{g`Nտ +M`;"PQI;IR#wۂlgN]*wCh7BU̘CM/86<$ se&=dǚo}4|k['/+b#hcM/oCjEI 1" ~fx &2ՓìjPb(xeq\:}Y86F#5mhp[ĕ>_mK˿Lg'%$5~SD*(.}@ CF,xG4vTai1:*6+ Nl\ł/jt1_FlO2vx+ٚ β#r/]SЂPhHQYSP˂+3n+!g~~Tb+zZGB5T>t+! 9<7Ś>l̽yXG&]O]FRry6c|&$xC+N*&5혃׭}CS}KYu.z] o{-QbYn9N\gBr0Tϥ1зeY80ăD;fuXҴ 53AC Ghn"gU:N8 sV|7^M L27L~2V[z}j}q_K%^\YV^׾"k+4F{25~E)q,E(Vi?lNʑyb`.g-&/[EtwTUϾWh:=#p7[lM?mzxD6ïgc)r1g-l,-zɬk&+ ԑpl,0ꦯBEPЧHiE}L#kɠ:۟UȞTיKWХ:١#Gogު}wW#05A"\4J9#"~L,ikp'Ɏ|ZJsEZ/ud|U椅C(]0ʈiAtLe.H>`)uh'|@؎ܓIkn46m\:"Կ:&'cTDh(|t[ 1d`+)V?!l WDA;lD";;+A4,6FZ6GGǐ[JME!X!fB'\ՖSJ=P+-b +=* g"eOgXS}[ %kˌp-0T#cҬ`C>$|j_hAHu#-^_&B f$D> 6 QG4.bǔ_,ݜgdlanfu!\QK.o%ͦ ~2Yo@WLqo83);Hu7]ް4sNa4m\C*uYY3?2/L-cL/@cff롩@W: T`u;~aR'M8h 8M]*;nβs ~'UTN5UYo Bib51o=Q$u:+w==K*3X] ok FVymBJ6M{:, M>Zn$( ʗb)s&"'W7A-{uh!|Uh{t^2V:DZ#E愳Pĉ+ "&/XYuXe %|W 4kQġ/M+pE(͞UmB1FtX{3)̹ NRxrsgdǮ?OcJNVÕH3YUԻP߅|J6Y97۫My BA1#ny*վ-uon;ux8Yɲ8ԥR_֞(wxf)O#e)5fLyGST}Z^kwJOiY]ҀHkTD'-H4ŀ}*Iص#bz#0Kltj2p)G],sD?3 jCNȋ+\k55h~:/F+N@ `: ' DڅlL_PX<B弼q$74 6;jJ\-}Q: H$9/{b2Lw)j![! lNDWD l'![],|-8*gQ\6eEu n|pu(Y/M>$ @}aeĢ;kIO2dIa1Of b7jg/8^*&VjuĬztGʙ\폆篆Y|#3D'ZU*g_?먄mH^y $X&F3 bW@VCP30bIqmh-=[HvN61naм'tŎmtMIl1&f 6A&i }lQe{hrkEH]Лh%2zx(lv ᭜b6-&.HEŸFٿiEĪC !LE@nFNtD֥8K7Gx`k̖(wN=H'TRAyux֕e؝6d%/y^G C^*feg fiJNEk$"74URxF:Ƽf 5&cI|FvX$MDYz_Y,x~.&郶?1'I"CgܫY]{pBV|NYWknW&3k $s$ab,U+U,w=Z1QT$eNpgC:6{AQ%{tki|HqWqrj5WH1}laC'cTH5]>"qV#b8Ȁ;"gsHSk6u );C#-5Tͧ'RiC*ڕ̟ N}q%9igR_f:n@ Rjtk,Lo֣/%_ .Ev-u8O|d?^b 0)4e󱈩,LͿ _1S?ȡn3La/Ka@tp):ȏ~=.. |F#(LZIC!1W~0eUܔ jYurj:*C8;b>KuDwMzNgv?wwCעR5tʕTx$ρ֕FP`FƑޣcWҮD'>)R2q1~@I4OD&/$yҥtَkŌCԏ1~ټ6JBQtNyqgʩF͠^{j ^L<=\ lw yM{miDBlFC^ = )]*PN3%:-T{cAXi73Chkh@W#80?x0Quq' ui wXO޳q QzZ] ɤ>&]1!eüa/sEMJU[d VCn ;\"2. /GDtwmA<HsFQ1Л9Hu6j$tz'2fVYc=np~0F(Ypfśi1$EU綼SfG!ENQW1j^wX:L}LcbC0 Y]C1}*썝8ytQY.XivL@;1|O1`4~h$V`gNJHp9O#sf'5Y2_R?wy}a+TeB hcuW"Ou6`4F͕d:ODLYY SC4JK*UORַL詹W::7 lUǃ [tsG Y:i6$2E8E!z AMjH3Cd7zRkhoiB8#/-S9eU{'ri(Ս `xEYái*I]1vdK0ī2 i7SyZ™q'VǤ!:Y(I;yo?nؤQT-@^t8#P"LW88#2|XGEШ \`It+ԸzzE썄vqWg;I!"*iF1UDoGWN7OF -i* H-wui).<:C8ѳQKqy Cwp-;K^/!.9HvrH c*f;pD8下 iF +R.[~e=Ѐ $稩[IIq 93e }cMs"V74:lE$&(2KZ5Lbʨ-^ CsV !]M[lc};oYNHp^Bb%`V} 6/6"|S/ KzLWKJzIĬ(NQAq~mI)׿r| 䢩$- _)Xo{=fi㵛0; w#@ڽFT^齁PDWb7Wg]Z#PNiN)̤qZ>"gLW7i<%1Ǘ&pc5/pP-hjumWmy wҁv@[{( WH[؊,4wk4=* Kƣ@idcbp}㱷5hS &[ ٙyעġ%;:\-r꼛TF}7goJ|IjLb P%IXbCXoHRwB 0,nb*Y;3hru vKQCΐ#=q<ׯLdsך{B/Ӭ!-5im]`;Ե?k RQ g2~RTzXbrEnw{^zV  -M$L4g_#Go(A~D3 )n̹n+Z#QV5U!Z,79pƤQ+OP#k]+pF {OXOoaF7D&dsa5PޙA=y@o*4??y!B C!G^`6ۏ:2h/-4eX"FwX^t̝+8TD6rg;Z 8İm΄qRPI,%oE]rnyAfQJI0smmL\ ޺!S7uP盧55rn>4mWγa$In21 9ا<$ 'k S_RD{3!;YWLsOq{2N9Y)kPu/)zW7^) Swۚ|K,P.qXۚ!Wbk7%}ܰ"ɃIkggb*ub14&%F:Ąd>o:uc gqᆉ*'AD61#ڎ6a67r<ٝ [`K ]3OHm'RtzVQot%A%WSx&YT nZF}ƞs$79:sqqmtxSuPSS/aqW==6WVEm@mGm쑮IlL|Y+Y~B䥟/SՀ4SQpoEYu4H`yA(.VX<k;.i[r .$#6,M @I?A./r%Qs ť Oջ}*1(,lfڳW%P͙tù| R]W3i1SY! Nxog*2eؠn#R +2}'u*rݥK zTioŠߞmy&l !t]D->8n,;&)ÃL0py90fOBtcV?#o:w?+>hP='g<P".U&n?̂W˒`Z u2L{A9p> Mk>翀 ZTǸl;s?\xƐh1eHҜpY*#}Q^РN`u.FXl&KjS>ANcCVvjTv(19F<"(`_wV)tjJ=J{`JuR*֜F]k{}vX$:^0!ܶ2\.yQY=#8{Gɽ7We0KH \ t VH QLI[eއv܊&lHњSő4/AmPr/yIEeaH8Ҡ^K,F]߂%B!\w3ֶ!7ʆL13o4b:>!m@o )31NKKP&IZj{NmLY c3Sc :jS+T9 ]E@hJd V5=CEqnu0gQ:|Wn77̿m淽%^%p͵U+ 6?_i&7v`>K07I\tQDJEw2# "̈T$hxKd<`C`7i3(>EgD(h:pnJ1#t.ZTX` 5fښ* /?k* =C8 S9@G+Ra%Jwpro.qξR=H੅P Eɟch$>"ܫ\ϥb ԚapreD8uI'kj&Pn W@؉v﹯X@!/(z^!^e+@U0?񕁝rh׮4h B桥0)Ɍxpj1 KV(CțQM r ҡ xO޴wX㮤|4Y<lo3qp 7KcEyA<6A )8A!s}ϛSI&I(Aĝ#T> j}(rgek`^:{vgGĂLX|m$ x{_|4bsׄJWj.7$=t8UŗŌM"^֕dD'A7<WւU+])2Wv1!js{@7 xV[\θrF `