Gentoo Archives: gentoo-commits

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