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 5773D138A1A for ; Sun, 25 Jan 2015 11:30:04 +0000 (UTC) Received: from pigeon.gentoo.org (localhost [127.0.0.1]) by pigeon.gentoo.org (Postfix) with SMTP id A7EB0E0848; Sun, 25 Jan 2015 11:30:03 +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 0F99AE0829 for ; Sun, 25 Jan 2015 11:30:03 +0000 (UTC) Received: from pomiot.lan (77-255-4-103.adsl.inetia.pl [77.255.4.103]) (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 68533340656; Sun, 25 Jan 2015 11:30:01 +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] Support escaping network-sandbox through SOCKSv5 proxy Date: Sun, 25 Jan 2015 12:29:54 +0100 Message-Id: <1422185394-6403-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: c0d4a894-15e9-44e8-a850-529865396736 X-Archives-Hash: 6133829a59a875695de25275599a9b35 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 currently supports IPv4 only, and does not report bound address (reports 0.0.0.0:0). No authentication is supported (UNIX sockets provide a security layer). --- bin/save-ebuild-env.sh | 5 +- bin/socks5-server.py | 171 +++++++++++++++++++++ .../package/ebuild/_config/special_env_vars.py | 2 +- pym/portage/package/ebuild/doebuild.py | 7 + pym/portage/util/socks5.py | 45 ++++++ 5 files changed, 227 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..4795dcc --- /dev/null +++ b/bin/socks5-server.py @@ -0,0 +1,171 @@ +#!/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): + _proxy_conn = None + + def __init__(self, host, port, proxy_conn): + self._proxy_conn = proxy_conn + asyncore.dispatcher_with_send.__init__(self) + # TODO: how to support IPv6? ugly fail-then-reinit? + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect((host, port)) + + def handle_read(self): + buf = self.recv(4096) + self._proxy_conn.send(buf) + + def handle_connect(self): + self._proxy_conn.send_connected() + + def handle_close(self): + self._proxy_conn.close() + + def handle_error(self): + e, v, tb = sys.exc_info() + if e is OSError: + self._proxy_conn.send_failure(v.errno) + self.close() + else: + raise + + +class ProxyHandler(asyncore.dispatcher_with_send): + _my_buf = b'' + _my_conn = None + _my_state = 0 + + def handle_read(self): + rd = self.recv(4096) + if not rd: + return + + 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_conn = ProxyConnection(addr, port, self) + + 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): + repl = struct.pack('!BBBBLH', 0x05, 0x00, 0x00, 0x01, + 0x00000000, 0x0000) + self.send(repl) + self._my_state = 3 + + def send_failure(self, err): + 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() + + +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 791b5c3..0d71f01 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