public inbox for gentoo-commits@lists.gentoo.org
 help / color / mirror / Atom feed
Search results ordered by [date|relevance]  view[summary|nested|Atom feed]
thread overview below | download mbox.gz: |
* [gentoo-commits] proj/portage:master commit in: pym/portage/package/ebuild/, pym/portage/util/, man/, bin/, ...
@ 2015-02-01  9:07 99% Michał Górny
  0 siblings, 0 replies; 1+ results
From: Michał Górny @ 2015-02-01  9:07 UTC (permalink / raw
  To: gentoo-commits

commit:     8fd09dc9a25fb673426340a23794df7f11a44010
Author:     Michał Górny <mgorny <AT> gentoo <DOT> org>
AuthorDate: Sun Jan 25 11:25:24 2015 +0000
Commit:     Michał Górny <mgorny <AT> gentoo <DOT> 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 <socket-path>' % 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


^ permalink raw reply related	[relevance 99%]

Results 1-1 of 1 | reverse | options above
-- pct% links below jump to the message on this page, permalinks otherwise --
2015-02-01  9:07 99% [gentoo-commits] proj/portage:master commit in: pym/portage/package/ebuild/, pym/portage/util/, man/, bin/, Michał Górny

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox