public inbox for gentoo-portage-dev@lists.gentoo.org
 help / color / mirror / Atom feed
From: "Michał Górny" <mgorny@gentoo.org>
To: gentoo-portage-dev@lists.gentoo.org
Cc: "Michał Górny" <mgorny@gentoo.org>
Subject: [gentoo-portage-dev] [PATCH v2] Support escaping network-sandbox through SOCKSv5 proxy
Date: Sun, 25 Jan 2015 15:00:14 +0100	[thread overview]
Message-ID: <1422194414-31669-1-git-send-email-mgorny@gentoo.org> (raw)

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 <socket-path>' % 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



             reply	other threads:[~2015-01-25 14:00 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2015-01-25 14:00 Michał Górny [this message]
2015-01-25 21:43 ` [gentoo-portage-dev] [PATCH v2] Support escaping network-sandbox through SOCKSv5 proxy Zac Medico
2015-01-25 22:01   ` Michał Górny
2015-01-25 21:47 ` Zac Medico
2015-01-25 21:58 ` Zac Medico

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=1422194414-31669-1-git-send-email-mgorny@gentoo.org \
    --to=mgorny@gentoo.org \
    --cc=gentoo-portage-dev@lists.gentoo.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox