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
next 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