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 95297138A1A for ; Sun, 25 Jan 2015 14:00:45 +0000 (UTC) Received: from pigeon.gentoo.org (localhost [127.0.0.1]) by pigeon.gentoo.org (Postfix) with SMTP id C5E9BE08DC; Sun, 25 Jan 2015 14:00:23 +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 3CCEDE08D6 for ; Sun, 25 Jan 2015 14:00:23 +0000 (UTC) Received: from pomiot.lan (mgorny-1-pt.tunnel.tserv28.waw1.ipv6.he.net [IPv6:2001:470:70:353::2]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-SHA256 (128/128 bits)) (No client certificate requested) (Authenticated sender: mgorny) by smtp.gentoo.org (Postfix) with ESMTPSA id 823023406C0; Sun, 25 Jan 2015 14:00:21 +0000 (UTC) From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= To: gentoo-portage-dev@lists.gentoo.org Cc: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Subject: [gentoo-portage-dev] [PATCH v2] Support escaping network-sandbox through SOCKSv5 proxy Date: Sun, 25 Jan 2015 15:00:14 +0100 Message-Id: <1422194414-31669-1-git-send-email-mgorny@gentoo.org> X-Mailer: git-send-email 2.2.2 Precedence: bulk List-Post: List-Help: List-Unsubscribe: List-Subscribe: List-Id: Gentoo Linux mail X-BeenThere: gentoo-portage-dev@lists.gentoo.org Reply-to: gentoo-portage-dev@lists.gentoo.org X-Archives-Salt: 5836b701-2551-41ee-be6d-4c1c2096a55e X-Archives-Hash: 84e20150cf29e39a7e9d18fce6f43f89 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 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 | 218 +++++++++++++++++++++ .../package/ebuild/_config/special_env_vars.py | 2 +- pym/portage/package/ebuild/doebuild.py | 7 + pym/portage/util/socks5.py | 45 +++++ 5 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 bin/socks5-server.py create mode 100644 pym/portage/util/socks5.py 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..c079018 --- /dev/null +++ b/bin/socks5-server.py @@ -0,0 +1,218 @@ +#!/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 asyncore +import errno +import socket +import struct +import sys + + +class ProxyConnection(asyncore.dispatcher_with_send): + _addr = None + _connected = False + _family = socket.AF_INET + _proxy_conn = None + + def __init__(self, proxy_conn): + self._proxy_conn = proxy_conn + asyncore.dispatcher_with_send.__init__(self) + self.create_socket(self._family, socket.SOCK_STREAM) + + def start_connection(self, host, port): + try: + self.connect((host, port)) + except: + self.handle_error() + + def handle_read(self): + buf = self.recv(4096) + self._proxy_conn.send(buf) + + def handle_connect(self): + self._connected = True + self._proxy_conn.send_connected(self._family, self.getsockname()) + + def handle_close(self): + if self._connected: + self._proxy_conn.remote_closed() + + def handle_error(self): + e, v, tb = sys.exc_info() + if isinstance(v, socket.gaierror) or isinstance(v, socket.herror): + self.close() + self._proxy_conn.send_failure(self._family, errno.EHOSTUNREACH) + elif isinstance(e, OSError): + self.close() + self._proxy_conn.send_failure(self._family, v.errno) + else: + raise + + +class ProxyConnectionV6(ProxyConnection): + _family = socket.AF_INET6 + + +class ProxyHandler(asyncore.dispatcher_with_send): + _my_buf = b'' + _my_conn = None + _my_state = 0 + _my_addr = None + + def handle_read(self): + rd = self.recv(4096) + + self._my_buf += rd + if self._my_state == 0: # waiting for hello + if len(self._my_buf) >= 3: + vers, method_no = struct.unpack('!BB', self._my_buf[:2]) + if vers != 0x05: + self.close() + return + if len(self._my_buf) >= 2 + method_no: + for method in self._my_buf[2:2+method_no]: + if method == 0x00: + break + else: + # no supported method + method = 0xFF + + repl = struct.pack('!BB', 0x05, method) + self.send(repl) + if method == 0xFF: + self.close() + return + else: + self._my_buf = self._my_buf[2+method_no:] + self._my_state = 1 + + if self._my_state == 1: # waiting for request + if len(self._my_buf) >= 5: + vers, cmd, rsv, atyp = struct.unpack('!BBBB', self._my_buf[:4]) + if vers != 0x05 or rsv != 0x00: + self.close() + return + + rpl = 0x00 + addr_len = 0 + if cmd != 0x01: # CONNECT + rpl = 0x07 # command not supported + elif atyp == 0x01: # IPv4 + addr_len = 4 + elif atyp == 0x03: # domain name + addr_len, = struct.unpack('!B', self._my_buf[4:5]) + addr_len += 1 # length field + elif atyp == 0x04: # IPv6 + addr_len = 16 + else: + rpl = 0x08 # address type not supported + + # terminate early + if rpl != 0x00: + repl = struct.pack('!BBBBLH', 0x05, rpl, 0x00, 0x01, + 0x00000000, 0x0000) + self.send(repl) + self.close() + return + + if len(self._my_buf) >= 6 + addr_len: + if atyp == 0x01: + addr = socket.inet_ntoa(self._my_buf[5:5+addr_len]) + elif atyp == 0x03: + addr = self._my_buf[5:4+addr_len] + elif atyp == 0x04: + addr = socket.inet_ntop(socket.AF_INET6, self._my_buf[5:5+addr_len]) + port, = struct.unpack('!H', self._my_buf[4+addr_len:6+addr_len]) + + self._my_buf = self._my_buf[6+addr_len:] + self._my_state = 2 + self._my_addr = (atyp, addr, port) + if atyp in (0x03, 0x04): + # try IPv6 first + self._my_conn = ProxyConnectionV6(self) + else: + self._my_conn = ProxyConnection(self) + self._my_conn.start_connection(addr, port) + + if self._my_state == 2: # connecting + pass + + if self._my_state == 3: # connected + self._my_conn.send(self._my_buf) + self._my_buf = b'' + + def handle_close(self): + if self._my_conn is not None: + self._my_conn.close() + + def send_connected(self, family, addr): + + if family == socket.AF_INET: + host, port = addr + bin_host = socket.inet_aton(host) + + repl = struct.pack('!BBBB4sH', 0x05, 0x00, 0x00, 0x01, + bin_host, port) + elif family == socket.AF_INET6: + # discard flowinfo, scope_id + host, port = addr[:2] + bin_host = socket.inet_pton(family, host) + + repl = struct.pack('!BBBB16sH', 0x05, 0x00, 0x00, 0x04, + bin_host, port) + + self.send(repl) + self._my_state = 3 + + # flush the buffer + if self._my_buf: + self.handle_read() + + def send_failure(self, family, err): + if family == socket.AF_INET6 and self._my_addr[0] != 0x04: + # retry as IPv4 + self._my_conn = ProxyConnection(self) + self._my_conn.start_connection(*self._my_addr[1:]) + return + + rpl = 0x01 # general error + if err in (errno.ENETUNREACH, errno.ENETDOWN): + rpl = 0x03 # network unreachable + elif err in (errno.EHOSTUNREACH, errno.EHOSTDOWN): + rpl = 0x04 # host unreachable + elif err in (errno.ECONNREFUSED, errno.ETIMEDOUT): + rpl = 0x05 # connection refused + + repl = struct.pack('!BBBBLH', 0x05, rpl, 0x00, 0x01, + 0x00000000, 0x0000) + self.send(repl) + self.close() + + def remote_closed(self): + self.close() + + +class ProxyServer(asyncore.dispatcher): + def __init__(self, socket_path): + asyncore.dispatcher.__init__(self) + self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.set_reuse_addr() + self.bind(socket_path) + self.listen(5) + + def handle_accepted(self, sock, addr): + h = ProxyHandler(sock) + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print('Usage: %s ' % sys.argv[0]) + sys.exit(1) + + try: + s = ProxyServer(sys.argv[1]) + asyncore.loop() + except KeyboardInterrupt: + sys.exit(0) 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 e58181b..050f6c4 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,12 @@ 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 + proxy = get_socks5_proxy(mysettings) + 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..c8b3d6a --- /dev/null +++ b/pym/portage/util/socks5.py @@ -0,0 +1,45 @@ +# 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): + socket_path = None + _pids = None + + def start(self, settings): + 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) + + atexit_register(self.stop) + + def stop(self): + self.socket_path = None + + for p in self._pids: + os.kill(p, signal.SIGINT) + os.waitpid(p, 0) + + def __bool__(self): + return self.socket_path is not None + + +running_proxy = ProxyManager() + + +def get_socks5_proxy(settings): + if not running_proxy: + running_proxy.start(settings) + + return running_proxy.socket_path -- 2.2.2