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 |