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] Support escaping network-sandbox through SOCKSv5 proxy
Date: Sun, 25 Jan 2015 12:29:54 +0100	[thread overview]
Message-ID: <1422185394-6403-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 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 <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 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



             reply	other threads:[~2015-01-25 11:30 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2015-01-25 11:29 Michał Górny [this message]
2015-01-25 14:01 ` [gentoo-portage-dev] [PATCH] Support escaping network-sandbox through SOCKSv5 proxy Michał Górny

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=1422185394-6403-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