Gentoo Archives: gentoo-portage-dev

From: "Michał Górny" <mgorny@g.o>
To: gentoo-portage-dev@l.g.o
Cc: "Michał Górny" <mgorny@g.o>
Subject: [gentoo-portage-dev] [PATCH v6] Support escaping network-sandbox through SOCKSv5 proxy
Date: Sun, 01 Feb 2015 08:54:16
Message-Id: 1422780839-19341-1-git-send-email-mgorny@gentoo.org
1 Add a minimal SOCKSv5-over-UNIX-socket proxy to Portage, and start it
2 whenever ebuilds are started with network-sandbox enabled. Pass the
3 socket address in PORTAGE_SOCKS5_PROXY and DISTCC_SOCKS_PROXY variables.
4 The proxy can be used to escape the network sandbox whenever network
5 access is really desired, e.g. in distcc.
6
7 The proxy is based on asynchronous I/O using the asyncio module.
8 Therefore, it requires the asyncio module that is built-in in Python 3.4
9 and available stand-alone for Python 3.3. Escaping the sandbox is not
10 supported with older versions of Python.
11
12 The proxy supports connecting to IPv6 & IPv4 TCP hosts. UDP and socket
13 binding are not supported. SOCKSv5 authentication schemes are not
14 supported (UNIX sockets provide a security layer).
15 ---
16 bin/save-ebuild-env.sh | 5 +-
17 bin/socks5-server.py | 227 +++++++++++++++++++++
18 man/ebuild.5 | 5 +
19 man/make.conf.5 | 7 +
20 .../package/ebuild/_config/special_env_vars.py | 2 +-
21 pym/portage/package/ebuild/doebuild.py | 11 +
22 pym/portage/util/socks5.py | 81 ++++++++
23 7 files changed, 335 insertions(+), 3 deletions(-)
24 create mode 100644 bin/socks5-server.py
25 create mode 100644 pym/portage/util/socks5.py
26
27 diff --git a/bin/save-ebuild-env.sh b/bin/save-ebuild-env.sh
28 index c6bffb5..477ed28 100644
29 --- a/bin/save-ebuild-env.sh
30 +++ b/bin/save-ebuild-env.sh
31 @@ -92,7 +92,7 @@ __save_ebuild_env() {
32
33 # portage config variables and variables set directly by portage
34 unset ACCEPT_LICENSE BAD BRACKET BUILD_PREFIX COLS \
35 - DISTCC_DIR DISTDIR DOC_SYMLINKS_DIR \
36 + DISTCC_DIR DISTCC_SOCKS5_PROXY DISTDIR DOC_SYMLINKS_DIR \
37 EBUILD_FORCE_TEST EBUILD_MASTER_PID \
38 ECLASS_DEPTH ENDCOL FAKEROOTKEY \
39 GOOD HILITE HOME \
40 @@ -105,7 +105,8 @@ __save_ebuild_env() {
41 PORTAGE_DOHTML_WARN_ON_SKIPPED_FILES \
42 PORTAGE_NONFATAL PORTAGE_QUIET \
43 PORTAGE_SANDBOX_DENY PORTAGE_SANDBOX_PREDICT \
44 - PORTAGE_SANDBOX_READ PORTAGE_SANDBOX_WRITE PREROOTPATH \
45 + PORTAGE_SANDBOX_READ PORTAGE_SANDBOX_WRITE \
46 + PORTAGE_SOCKS5_PROXY PREROOTPATH \
47 QA_INTERCEPTORS \
48 RC_DEFAULT_INDENT RC_DOT_PATTERN RC_ENDCOL RC_INDENTATION \
49 ROOT ROOTPATH RPMDIR TEMP TMP TMPDIR USE_EXPAND \
50 diff --git a/bin/socks5-server.py b/bin/socks5-server.py
51 new file mode 100644
52 index 0000000..71e6b01
53 --- /dev/null
54 +++ b/bin/socks5-server.py
55 @@ -0,0 +1,227 @@
56 +#!/usr/bin/env python
57 +# SOCKSv5 proxy server for network-sandbox
58 +# Copyright 2015 Gentoo Foundation
59 +# Distributed under the terms of the GNU General Public License v2
60 +
61 +import asyncio
62 +import errno
63 +import os
64 +import socket
65 +import struct
66 +import sys
67 +
68 +
69 +class Socks5Server(object):
70 + """
71 + An asynchronous SOCKSv5 server.
72 + """
73 +
74 + @asyncio.coroutine
75 + def handle_proxy_conn(self, reader, writer):
76 + """
77 + Handle incoming client connection. Perform SOCKSv5 request
78 + exchange, open a proxied connection and start relaying.
79 +
80 + @param reader: Read side of the socket
81 + @type reader: asyncio.StreamReader
82 + @param writer: Write side of the socket
83 + @type writer: asyncio.StreamWriter
84 + """
85 +
86 + try:
87 + # SOCKS hello
88 + data = yield from reader.readexactly(2)
89 + vers, method_no = struct.unpack('!BB', data)
90 +
91 + if vers != 0x05:
92 + # disconnect on invalid packet -- we have no clue how
93 + # to reply in alien :)
94 + writer.close()
95 + return
96 +
97 + # ...and auth method list
98 + data = yield from reader.readexactly(method_no)
99 + for method in data:
100 + if method == 0x00:
101 + break
102 + else:
103 + # no supported method
104 + method = 0xFF
105 +
106 + # auth reply
107 + repl = struct.pack('!BB', 0x05, method)
108 + writer.write(repl)
109 + yield from writer.drain()
110 + if method == 0xFF:
111 + writer.close()
112 + return
113 +
114 + # request
115 + data = yield from reader.readexactly(4)
116 + vers, cmd, rsv, atyp = struct.unpack('!BBBB', data)
117 +
118 + if vers != 0x05 or rsv != 0x00:
119 + # disconnect on malformed packet
120 + self.close()
121 + return
122 +
123 + # figure out if we can handle it
124 + rpl = 0x00
125 + if cmd != 0x01: # CONNECT
126 + rpl = 0x07 # command not supported
127 + elif atyp == 0x01: # IPv4
128 + data = yield from reader.readexactly(4)
129 + addr = socket.inet_ntoa(data)
130 + elif atyp == 0x03: # domain name
131 + data = yield from reader.readexactly(1)
132 + addr_len, = struct.unpack('!B', data)
133 + addr = yield from reader.readexactly(addr_len)
134 + elif atyp == 0x04: # IPv6
135 + data = yield from reader.readexactly(16)
136 + addr = socket.inet_ntop(socket.AF_INET6, data)
137 + else:
138 + rpl = 0x08 # address type not supported
139 +
140 + # try to connect if we can handle it
141 + if rpl == 0x00:
142 + data = yield from reader.readexactly(2)
143 + port, = struct.unpack('!H', data)
144 +
145 + try:
146 + # open a proxied connection
147 + proxied_reader, proxied_writer = yield from asyncio.open_connection(
148 + addr, port)
149 + except (socket.gaierror, socket.herror):
150 + # DNS failure
151 + rpl = 0x04 # host unreachable
152 + except OSError as e:
153 + # connection failure
154 + if e.errno in (errno.ENETUNREACH, errno.ENETDOWN):
155 + rpl = 0x03 # network unreachable
156 + elif e.errno in (errno.EHOSTUNREACH, errno.EHOSTDOWN):
157 + rpl = 0x04 # host unreachable
158 + elif e.errno in (errno.ECONNREFUSED, errno.ETIMEDOUT):
159 + rpl = 0x05 # connection refused
160 + else:
161 + raise
162 + else:
163 + # get socket details that we can send back to the client
164 + # local address (sockname) in particular -- but we need
165 + # to ask for the whole socket since Python's sockaddr
166 + # does not list the family...
167 + sock = proxied_writer.get_extra_info('socket')
168 + addr = sock.getsockname()
169 + if sock.family == socket.AF_INET:
170 + host, port = addr
171 + bin_host = socket.inet_aton(host)
172 +
173 + repl_addr = struct.pack('!B4sH',
174 + 0x01, bin_host, port)
175 + elif sock.family == socket.AF_INET6:
176 + # discard flowinfo, scope_id
177 + host, port = addr[:2]
178 + bin_host = socket.inet_pton(sock.family, host)
179 +
180 + repl_addr = struct.pack('!B16sH',
181 + 0x04, bin_host, port)
182 +
183 + if rpl != 0x00:
184 + # fallback to 0.0.0.0:0
185 + repl_addr = struct.pack('!BLH', 0x01, 0x00000000, 0x0000)
186 +
187 + # reply to the request
188 + repl = struct.pack('!BBB', 0x05, rpl, 0x00)
189 + writer.write(repl + repl_addr)
190 + yield from writer.drain()
191 +
192 + # close if an error occured
193 + if rpl != 0x00:
194 + writer.close()
195 + return
196 +
197 + # otherwise, start two loops:
198 + # remote -> local...
199 + t = asyncio.async(self.handle_proxied_conn(
200 + proxied_reader, writer, asyncio.Task.current_task()))
201 +
202 + # and local -> remote...
203 + try:
204 + try:
205 + while True:
206 + data = yield from reader.read(4096)
207 + if data == b'':
208 + # client disconnected, stop relaying from
209 + # remote host
210 + t.cancel()
211 + break
212 +
213 + proxied_writer.write(data)
214 + yield from proxied_writer.drain()
215 + except OSError:
216 + # read or write failure
217 + t.cancel()
218 + except:
219 + t.cancel()
220 + raise
221 + finally:
222 + # always disconnect in the end :)
223 + proxied_writer.close()
224 + writer.close()
225 +
226 + except (OSError, asyncio.IncompleteReadError, asyncio.CancelledError):
227 + writer.close()
228 + return
229 + except:
230 + writer.close()
231 + raise
232 +
233 + @asyncio.coroutine
234 + def handle_proxied_conn(self, proxied_reader, writer, parent_task):
235 + """
236 + Handle the proxied connection. Relay incoming data
237 + to the client.
238 +
239 + @param reader: Read side of the socket
240 + @type reader: asyncio.StreamReader
241 + @param writer: Write side of the socket
242 + @type writer: asyncio.StreamWriter
243 + """
244 +
245 + try:
246 + try:
247 + while True:
248 + data = yield from proxied_reader.read(4096)
249 + if data == b'':
250 + break
251 +
252 + writer.write(data)
253 + yield from writer.drain()
254 + finally:
255 + parent_task.cancel()
256 + except (OSError, asyncio.CancelledError):
257 + return
258 +
259 +
260 +if __name__ == '__main__':
261 + if len(sys.argv) != 2:
262 + print('Usage: %s <socket-path>' % sys.argv[0])
263 + sys.exit(1)
264 +
265 + loop = asyncio.get_event_loop()
266 + s = Socks5Server()
267 + server = loop.run_until_complete(
268 + asyncio.start_unix_server(s.handle_proxy_conn, sys.argv[1], loop=loop))
269 +
270 + ret = 0
271 + try:
272 + try:
273 + loop.run_forever()
274 + except KeyboardInterrupt:
275 + pass
276 + except:
277 + ret = 1
278 + finally:
279 + server.close()
280 + loop.run_until_complete(server.wait_closed())
281 + loop.close()
282 + os.unlink(sys.argv[1])
283 diff --git a/man/ebuild.5 b/man/ebuild.5
284 index b587264..3f35180 100644
285 --- a/man/ebuild.5
286 +++ b/man/ebuild.5
287 @@ -484,6 +484,11 @@ source source\-build which is scheduled for merge
288 Contains the path of the build log. If \fBPORT_LOGDIR\fR variable is unset then
289 PORTAGE_LOG_FILE=\fI"${T}/build.log"\fR.
290 .TP
291 +.B PORTAGE_SOCKS5_PROXY
292 +Contains the UNIX socket path to SOCKSv5 proxy providing host network
293 +access. Available only when running inside network\-sandbox and a proxy
294 +is available (see network\-sandbox in \fBmake.conf\fR(5)).
295 +.TP
296 .B REPLACED_BY_VERSION
297 Beginning with \fBEAPI 4\fR, the REPLACED_BY_VERSION variable can be
298 used in pkg_prerm and pkg_postrm to query the package version that
299 diff --git a/man/make.conf.5 b/man/make.conf.5
300 index ed5fc78..84b7191 100644
301 --- a/man/make.conf.5
302 +++ b/man/make.conf.5
303 @@ -436,6 +436,13 @@ from putting 64bit libraries into anything other than (/usr)/lib64.
304 .B network\-sandbox
305 Isolate the ebuild phase functions from host network interfaces.
306 Supported only on Linux. Requires network namespace support in kernel.
307 +
308 +If asyncio Python module is available (requires Python 3.3, built-in
309 +since Python 3.4) Portage will additionally spawn an isolated SOCKSv5
310 +proxy on UNIX socket. The socket address will be exported
311 +as PORTAGE_SOCKS5_PROXY and the processes running inside the sandbox
312 +can use it to access host's network when desired. Portage automatically
313 +configures new enough distcc to use the proxy.
314 .TP
315 .B news
316 Enable GLEP 42 news support. See
317 diff --git a/pym/portage/package/ebuild/_config/special_env_vars.py b/pym/portage/package/ebuild/_config/special_env_vars.py
318 index 6bb3c95..905d5e7 100644
319 --- a/pym/portage/package/ebuild/_config/special_env_vars.py
320 +++ b/pym/portage/package/ebuild/_config/special_env_vars.py
321 @@ -71,7 +71,7 @@ environ_whitelist += [
322 "PORTAGE_PYM_PATH", "PORTAGE_PYTHON",
323 "PORTAGE_PYTHONPATH", "PORTAGE_QUIET",
324 "PORTAGE_REPO_NAME", "PORTAGE_REPOSITORIES", "PORTAGE_RESTRICT",
325 - "PORTAGE_SIGPIPE_STATUS",
326 + "PORTAGE_SIGPIPE_STATUS", "PORTAGE_SOCKS5_PROXY",
327 "PORTAGE_TMPDIR", "PORTAGE_UPDATE_ENV", "PORTAGE_USERNAME",
328 "PORTAGE_VERBOSE", "PORTAGE_WORKDIR_MODE", "PORTAGE_XATTR_EXCLUDE",
329 "PORTDIR", "PORTDIR_OVERLAY", "PREROOTPATH",
330 diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py
331 index 791b5c3..6329b62 100644
332 --- a/pym/portage/package/ebuild/doebuild.py
333 +++ b/pym/portage/package/ebuild/doebuild.py
334 @@ -68,6 +68,7 @@ from portage.util import apply_recursive_permissions, \
335 writemsg, writemsg_stdout, write_atomic
336 from portage.util.cpuinfo import get_cpu_count
337 from portage.util.lafilefixer import rewrite_lafile
338 +from portage.util.socks5 import get_socks5_proxy
339 from portage.versions import _pkgsplit
340 from _emerge.BinpkgEnvExtractor import BinpkgEnvExtractor
341 from _emerge.EbuildBuildDir import EbuildBuildDir
342 @@ -1487,6 +1488,16 @@ def spawn(mystring, mysettings, debug=False, free=False, droppriv=False,
343 keywords['unshare_net'] = not networked
344 keywords['unshare_ipc'] = not ipc
345
346 + if not networked:
347 + # Provide a SOCKS5-over-UNIX-socket proxy to escape sandbox
348 + try:
349 + proxy = get_socks5_proxy(mysettings)
350 + except NotImplementedError:
351 + pass
352 + else:
353 + mysettings['PORTAGE_SOCKS5_PROXY'] = proxy
354 + mysettings['DISTCC_SOCKS_PROXY'] = proxy
355 +
356 # TODO: Enable fakeroot to be used together with droppriv. The
357 # fake ownership/permissions will have to be converted to real
358 # permissions in the merge phase.
359 diff --git a/pym/portage/util/socks5.py b/pym/portage/util/socks5.py
360 new file mode 100644
361 index 0000000..74b0714
362 --- /dev/null
363 +++ b/pym/portage/util/socks5.py
364 @@ -0,0 +1,81 @@
365 +# SOCKSv5 proxy manager for network-sandbox
366 +# Copyright 2015 Gentoo Foundation
367 +# Distributed under the terms of the GNU General Public License v2
368 +
369 +import os
370 +import signal
371 +
372 +from portage import _python_interpreter
373 +from portage.data import portage_gid, portage_uid, userpriv_groups
374 +from portage.process import atexit_register, spawn
375 +
376 +
377 +class ProxyManager(object):
378 + """
379 + A class to start and control a single running SOCKSv5 server process
380 + for Portage.
381 + """
382 +
383 + def __init__(self):
384 + self.socket_path = None
385 + self._pids = []
386 +
387 + def start(self, settings):
388 + """
389 + Start the SOCKSv5 server.
390 +
391 + @param settings: Portage settings instance (used to determine
392 + paths)
393 + @type settings: portage.config
394 + """
395 + try:
396 + import asyncio # NOQA
397 + except ImportError:
398 + raise NotImplementedError('SOCKSv5 proxy requires asyncio module')
399 +
400 + self.socket_path = os.path.join(settings['PORTAGE_TMPDIR'],
401 + '.portage.%d.net.sock' % os.getpid())
402 + server_bin = os.path.join(settings['PORTAGE_BIN_PATH'], 'socks5-server.py')
403 + self._pids = spawn([_python_interpreter, server_bin, self.socket_path],
404 + returnpid=True, uid=portage_uid, gid=portage_gid,
405 + groups=userpriv_groups, umask=0o077)
406 +
407 + def stop(self):
408 + """
409 + Stop the SOCKSv5 server.
410 + """
411 + for p in self._pids:
412 + os.kill(p, signal.SIGINT)
413 + os.waitpid(p, 0)
414 +
415 + self.socket_path = None
416 + self._pids = []
417 +
418 + def is_running(self):
419 + """
420 + Check whether the SOCKSv5 server is running.
421 +
422 + @return: True if the server is running, False otherwise
423 + """
424 + return self.socket_path is not None
425 +
426 +
427 +proxy = ProxyManager()
428 +
429 +
430 +def get_socks5_proxy(settings):
431 + """
432 + Get UNIX socket path for a SOCKSv5 proxy. A new proxy is started if
433 + one isn't running yet, and an atexit event is added to stop the proxy
434 + on exit.
435 +
436 + @param settings: Portage settings instance (used to determine paths)
437 + @type settings: portage.config
438 + @return: (string) UNIX socket path
439 + """
440 +
441 + if not proxy.is_running():
442 + proxy.start(settings)
443 + atexit_register(proxy.stop)
444 +
445 + return proxy.socket_path
446 --
447 2.2.2

Replies