* [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