From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from lists.gentoo.org (pigeon.gentoo.org [208.92.234.80]) by finch.gentoo.org (Postfix) with ESMTP id 7F8B1138A1A for ; Sun, 1 Feb 2015 09:07:20 +0000 (UTC) Received: from pigeon.gentoo.org (localhost [127.0.0.1]) by pigeon.gentoo.org (Postfix) with SMTP id 05F65E076D; Sun, 1 Feb 2015 09:07:19 +0000 (UTC) Received: from smtp.gentoo.org (smtp.gentoo.org [140.211.166.183]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by pigeon.gentoo.org (Postfix) with ESMTPS id 6778EE07DF for ; Sun, 1 Feb 2015 09:07:18 +0000 (UTC) Received: from oystercatcher.gentoo.org (oystercatcher.gentoo.org [148.251.78.52]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by smtp.gentoo.org (Postfix) with ESMTPS id 7C9D43407CA for ; Sun, 1 Feb 2015 09:07:17 +0000 (UTC) Received: from localhost.localdomain (localhost [127.0.0.1]) by oystercatcher.gentoo.org (Postfix) with ESMTP id 7D74F10D00 for ; Sun, 1 Feb 2015 09:07:15 +0000 (UTC) From: "Michał Górny" To: gentoo-commits@lists.gentoo.org Content-Transfer-Encoding: 8bit Content-type: text/plain; charset=UTF-8 Reply-To: gentoo-dev@lists.gentoo.org, "Michał Górny" Message-ID: <1422781601.8fd09dc9a25fb673426340a23794df7f11a44010.mgorny@gentoo> Subject: [gentoo-commits] proj/portage:master commit in: pym/portage/package/ebuild/, pym/portage/util/, man/, bin/, ... X-VCS-Repository: proj/portage X-VCS-Files: bin/save-ebuild-env.sh bin/socks5-server.py man/ebuild.5 man/make.conf.5 pym/portage/package/ebuild/_config/special_env_vars.py pym/portage/package/ebuild/doebuild.py pym/portage/util/socks5.py X-VCS-Directories: pym/portage/package/ebuild/ pym/portage/util/ man/ bin/ pym/portage/package/ebuild/_config/ X-VCS-Committer: mgorny X-VCS-Committer-Name: Michał Górny X-VCS-Revision: 8fd09dc9a25fb673426340a23794df7f11a44010 X-VCS-Branch: master Date: Sun, 1 Feb 2015 09:07:15 +0000 (UTC) Precedence: bulk List-Post: List-Help: List-Unsubscribe: List-Subscribe: List-Id: Gentoo Linux mail X-BeenThere: gentoo-commits@lists.gentoo.org X-Archives-Salt: 37947a99-90ad-4d36-a73e-9446aed80f10 X-Archives-Hash: 0b42e4e743a2dbbd3192ebc04e2db381 commit: 8fd09dc9a25fb673426340a23794df7f11a44010 Author: Michał Górny gentoo org> AuthorDate: Sun Jan 25 11:25:24 2015 +0000 Commit: Michał Górny gentoo org> CommitDate: Sun Feb 1 09:06:41 2015 +0000 URL: http://sources.gentoo.org/gitweb/?p=proj/portage.git;a=commit;h=8fd09dc9 Support escaping network-sandbox through SOCKSv5 proxy Add a minimal SOCKSv5-over-UNIX-socket proxy to Portage, and start it whenever ebuilds are started with network-sandbox enabled. Pass the socket address in PORTAGE_SOCKS5_PROXY and DISTCC_SOCKS_PROXY variables. The proxy can be used to escape the network sandbox whenever network access is really desired, e.g. in distcc. The proxy is based on asynchronous I/O using the asyncio module. Therefore, it requires the asyncio module that is built-in in Python 3.4 and available stand-alone for Python 3.3. Escaping the sandbox is not supported with older versions of Python. The proxy supports connecting to IPv6 & IPv4 TCP hosts. UDP and socket binding are not supported. SOCKSv5 authentication schemes are not supported (UNIX sockets provide a security layer). --- bin/save-ebuild-env.sh | 5 +- bin/socks5-server.py | 227 +++++++++++++++++++++ man/ebuild.5 | 5 + man/make.conf.5 | 7 + .../package/ebuild/_config/special_env_vars.py | 2 +- pym/portage/package/ebuild/doebuild.py | 11 + pym/portage/util/socks5.py | 81 ++++++++ 7 files changed, 335 insertions(+), 3 deletions(-) diff --git a/bin/save-ebuild-env.sh b/bin/save-ebuild-env.sh index c6bffb5..477ed28 100644 --- a/bin/save-ebuild-env.sh +++ b/bin/save-ebuild-env.sh @@ -92,7 +92,7 @@ __save_ebuild_env() { # portage config variables and variables set directly by portage unset ACCEPT_LICENSE BAD BRACKET BUILD_PREFIX COLS \ - DISTCC_DIR DISTDIR DOC_SYMLINKS_DIR \ + DISTCC_DIR DISTCC_SOCKS5_PROXY DISTDIR DOC_SYMLINKS_DIR \ EBUILD_FORCE_TEST EBUILD_MASTER_PID \ ECLASS_DEPTH ENDCOL FAKEROOTKEY \ GOOD HILITE HOME \ @@ -105,7 +105,8 @@ __save_ebuild_env() { PORTAGE_DOHTML_WARN_ON_SKIPPED_FILES \ PORTAGE_NONFATAL PORTAGE_QUIET \ PORTAGE_SANDBOX_DENY PORTAGE_SANDBOX_PREDICT \ - PORTAGE_SANDBOX_READ PORTAGE_SANDBOX_WRITE PREROOTPATH \ + PORTAGE_SANDBOX_READ PORTAGE_SANDBOX_WRITE \ + PORTAGE_SOCKS5_PROXY PREROOTPATH \ QA_INTERCEPTORS \ RC_DEFAULT_INDENT RC_DOT_PATTERN RC_ENDCOL RC_INDENTATION \ ROOT ROOTPATH RPMDIR TEMP TMP TMPDIR USE_EXPAND \ diff --git a/bin/socks5-server.py b/bin/socks5-server.py new file mode 100644 index 0000000..71e6b01 --- /dev/null +++ b/bin/socks5-server.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python +# SOCKSv5 proxy server for network-sandbox +# Copyright 2015 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import asyncio +import errno +import os +import socket +import struct +import sys + + +class Socks5Server(object): + """ + An asynchronous SOCKSv5 server. + """ + + @asyncio.coroutine + def handle_proxy_conn(self, reader, writer): + """ + Handle incoming client connection. Perform SOCKSv5 request + exchange, open a proxied connection and start relaying. + + @param reader: Read side of the socket + @type reader: asyncio.StreamReader + @param writer: Write side of the socket + @type writer: asyncio.StreamWriter + """ + + try: + # SOCKS hello + data = yield from reader.readexactly(2) + vers, method_no = struct.unpack('!BB', data) + + if vers != 0x05: + # disconnect on invalid packet -- we have no clue how + # to reply in alien :) + writer.close() + return + + # ...and auth method list + data = yield from reader.readexactly(method_no) + for method in data: + if method == 0x00: + break + else: + # no supported method + method = 0xFF + + # auth reply + repl = struct.pack('!BB', 0x05, method) + writer.write(repl) + yield from writer.drain() + if method == 0xFF: + writer.close() + return + + # request + data = yield from reader.readexactly(4) + vers, cmd, rsv, atyp = struct.unpack('!BBBB', data) + + if vers != 0x05 or rsv != 0x00: + # disconnect on malformed packet + self.close() + return + + # figure out if we can handle it + rpl = 0x00 + if cmd != 0x01: # CONNECT + rpl = 0x07 # command not supported + elif atyp == 0x01: # IPv4 + data = yield from reader.readexactly(4) + addr = socket.inet_ntoa(data) + elif atyp == 0x03: # domain name + data = yield from reader.readexactly(1) + addr_len, = struct.unpack('!B', data) + addr = yield from reader.readexactly(addr_len) + elif atyp == 0x04: # IPv6 + data = yield from reader.readexactly(16) + addr = socket.inet_ntop(socket.AF_INET6, data) + else: + rpl = 0x08 # address type not supported + + # try to connect if we can handle it + if rpl == 0x00: + data = yield from reader.readexactly(2) + port, = struct.unpack('!H', data) + + try: + # open a proxied connection + proxied_reader, proxied_writer = yield from asyncio.open_connection( + addr, port) + except (socket.gaierror, socket.herror): + # DNS failure + rpl = 0x04 # host unreachable + except OSError as e: + # connection failure + if e.errno in (errno.ENETUNREACH, errno.ENETDOWN): + rpl = 0x03 # network unreachable + elif e.errno in (errno.EHOSTUNREACH, errno.EHOSTDOWN): + rpl = 0x04 # host unreachable + elif e.errno in (errno.ECONNREFUSED, errno.ETIMEDOUT): + rpl = 0x05 # connection refused + else: + raise + else: + # get socket details that we can send back to the client + # local address (sockname) in particular -- but we need + # to ask for the whole socket since Python's sockaddr + # does not list the family... + sock = proxied_writer.get_extra_info('socket') + addr = sock.getsockname() + if sock.family == socket.AF_INET: + host, port = addr + bin_host = socket.inet_aton(host) + + repl_addr = struct.pack('!B4sH', + 0x01, bin_host, port) + elif sock.family == socket.AF_INET6: + # discard flowinfo, scope_id + host, port = addr[:2] + bin_host = socket.inet_pton(sock.family, host) + + repl_addr = struct.pack('!B16sH', + 0x04, bin_host, port) + + if rpl != 0x00: + # fallback to 0.0.0.0:0 + repl_addr = struct.pack('!BLH', 0x01, 0x00000000, 0x0000) + + # reply to the request + repl = struct.pack('!BBB', 0x05, rpl, 0x00) + writer.write(repl + repl_addr) + yield from writer.drain() + + # close if an error occured + if rpl != 0x00: + writer.close() + return + + # otherwise, start two loops: + # remote -> local... + t = asyncio.async(self.handle_proxied_conn( + proxied_reader, writer, asyncio.Task.current_task())) + + # and local -> remote... + try: + try: + while True: + data = yield from reader.read(4096) + if data == b'': + # client disconnected, stop relaying from + # remote host + t.cancel() + break + + proxied_writer.write(data) + yield from proxied_writer.drain() + except OSError: + # read or write failure + t.cancel() + except: + t.cancel() + raise + finally: + # always disconnect in the end :) + proxied_writer.close() + writer.close() + + except (OSError, asyncio.IncompleteReadError, asyncio.CancelledError): + writer.close() + return + except: + writer.close() + raise + + @asyncio.coroutine + def handle_proxied_conn(self, proxied_reader, writer, parent_task): + """ + Handle the proxied connection. Relay incoming data + to the client. + + @param reader: Read side of the socket + @type reader: asyncio.StreamReader + @param writer: Write side of the socket + @type writer: asyncio.StreamWriter + """ + + try: + try: + while True: + data = yield from proxied_reader.read(4096) + if data == b'': + break + + writer.write(data) + yield from writer.drain() + finally: + parent_task.cancel() + except (OSError, asyncio.CancelledError): + return + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print('Usage: %s ' % sys.argv[0]) + sys.exit(1) + + loop = asyncio.get_event_loop() + s = Socks5Server() + server = loop.run_until_complete( + asyncio.start_unix_server(s.handle_proxy_conn, sys.argv[1], loop=loop)) + + ret = 0 + try: + try: + loop.run_forever() + except KeyboardInterrupt: + pass + except: + ret = 1 + finally: + server.close() + loop.run_until_complete(server.wait_closed()) + loop.close() + os.unlink(sys.argv[1]) diff --git a/man/ebuild.5 b/man/ebuild.5 index b587264..3f35180 100644 --- a/man/ebuild.5 +++ b/man/ebuild.5 @@ -484,6 +484,11 @@ source source\-build which is scheduled for merge Contains the path of the build log. If \fBPORT_LOGDIR\fR variable is unset then PORTAGE_LOG_FILE=\fI"${T}/build.log"\fR. .TP +.B PORTAGE_SOCKS5_PROXY +Contains the UNIX socket path to SOCKSv5 proxy providing host network +access. Available only when running inside network\-sandbox and a proxy +is available (see network\-sandbox in \fBmake.conf\fR(5)). +.TP .B REPLACED_BY_VERSION Beginning with \fBEAPI 4\fR, the REPLACED_BY_VERSION variable can be used in pkg_prerm and pkg_postrm to query the package version that diff --git a/man/make.conf.5 b/man/make.conf.5 index ed5fc78..84b7191 100644 --- a/man/make.conf.5 +++ b/man/make.conf.5 @@ -436,6 +436,13 @@ from putting 64bit libraries into anything other than (/usr)/lib64. .B network\-sandbox Isolate the ebuild phase functions from host network interfaces. Supported only on Linux. Requires network namespace support in kernel. + +If asyncio Python module is available (requires Python 3.3, built-in +since Python 3.4) Portage will additionally spawn an isolated SOCKSv5 +proxy on UNIX socket. The socket address will be exported +as PORTAGE_SOCKS5_PROXY and the processes running inside the sandbox +can use it to access host's network when desired. Portage automatically +configures new enough distcc to use the proxy. .TP .B news Enable GLEP 42 news support. See diff --git a/pym/portage/package/ebuild/_config/special_env_vars.py b/pym/portage/package/ebuild/_config/special_env_vars.py index 6bb3c95..905d5e7 100644 --- a/pym/portage/package/ebuild/_config/special_env_vars.py +++ b/pym/portage/package/ebuild/_config/special_env_vars.py @@ -71,7 +71,7 @@ environ_whitelist += [ "PORTAGE_PYM_PATH", "PORTAGE_PYTHON", "PORTAGE_PYTHONPATH", "PORTAGE_QUIET", "PORTAGE_REPO_NAME", "PORTAGE_REPOSITORIES", "PORTAGE_RESTRICT", - "PORTAGE_SIGPIPE_STATUS", + "PORTAGE_SIGPIPE_STATUS", "PORTAGE_SOCKS5_PROXY", "PORTAGE_TMPDIR", "PORTAGE_UPDATE_ENV", "PORTAGE_USERNAME", "PORTAGE_VERBOSE", "PORTAGE_WORKDIR_MODE", "PORTAGE_XATTR_EXCLUDE", "PORTDIR", "PORTDIR_OVERLAY", "PREROOTPATH", diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py index f7561fe..0ac0166 100644 --- a/pym/portage/package/ebuild/doebuild.py +++ b/pym/portage/package/ebuild/doebuild.py @@ -68,6 +68,7 @@ from portage.util import apply_recursive_permissions, \ writemsg, writemsg_stdout, write_atomic from portage.util.cpuinfo import get_cpu_count from portage.util.lafilefixer import rewrite_lafile +from portage.util.socks5 import get_socks5_proxy from portage.versions import _pkgsplit from _emerge.BinpkgEnvExtractor import BinpkgEnvExtractor from _emerge.EbuildBuildDir import EbuildBuildDir @@ -1487,6 +1488,16 @@ def spawn(mystring, mysettings, debug=False, free=False, droppriv=False, keywords['unshare_net'] = not networked keywords['unshare_ipc'] = not ipc + if not networked: + # Provide a SOCKS5-over-UNIX-socket proxy to escape sandbox + try: + proxy = get_socks5_proxy(mysettings) + except NotImplementedError: + pass + else: + mysettings['PORTAGE_SOCKS5_PROXY'] = proxy + mysettings['DISTCC_SOCKS_PROXY'] = proxy + # TODO: Enable fakeroot to be used together with droppriv. The # fake ownership/permissions will have to be converted to real # permissions in the merge phase. diff --git a/pym/portage/util/socks5.py b/pym/portage/util/socks5.py new file mode 100644 index 0000000..74b0714 --- /dev/null +++ b/pym/portage/util/socks5.py @@ -0,0 +1,81 @@ +# SOCKSv5 proxy manager for network-sandbox +# Copyright 2015 Gentoo Foundation +# Distributed under the terms of the GNU General Public License v2 + +import os +import signal + +from portage import _python_interpreter +from portage.data import portage_gid, portage_uid, userpriv_groups +from portage.process import atexit_register, spawn + + +class ProxyManager(object): + """ + A class to start and control a single running SOCKSv5 server process + for Portage. + """ + + def __init__(self): + self.socket_path = None + self._pids = [] + + def start(self, settings): + """ + Start the SOCKSv5 server. + + @param settings: Portage settings instance (used to determine + paths) + @type settings: portage.config + """ + try: + import asyncio # NOQA + except ImportError: + raise NotImplementedError('SOCKSv5 proxy requires asyncio module') + + self.socket_path = os.path.join(settings['PORTAGE_TMPDIR'], + '.portage.%d.net.sock' % os.getpid()) + server_bin = os.path.join(settings['PORTAGE_BIN_PATH'], 'socks5-server.py') + self._pids = spawn([_python_interpreter, server_bin, self.socket_path], + returnpid=True, uid=portage_uid, gid=portage_gid, + groups=userpriv_groups, umask=0o077) + + def stop(self): + """ + Stop the SOCKSv5 server. + """ + for p in self._pids: + os.kill(p, signal.SIGINT) + os.waitpid(p, 0) + + self.socket_path = None + self._pids = [] + + def is_running(self): + """ + Check whether the SOCKSv5 server is running. + + @return: True if the server is running, False otherwise + """ + return self.socket_path is not None + + +proxy = ProxyManager() + + +def get_socks5_proxy(settings): + """ + Get UNIX socket path for a SOCKSv5 proxy. A new proxy is started if + one isn't running yet, and an atexit event is added to stop the proxy + on exit. + + @param settings: Portage settings instance (used to determine paths) + @type settings: portage.config + @return: (string) UNIX socket path + """ + + if not proxy.is_running(): + proxy.start(settings) + atexit_register(proxy.stop) + + return proxy.socket_path