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 supports connecting to IPv6 & IPv4 TCP hosts. UDP and socket |
8 |
binding are not supported. SOCKSv5 authentication schemes are not |
9 |
supported (UNIX sockets provide a security layer). |
10 |
|
11 |
--- |
12 |
New in v3: |
13 |
|
14 |
- added __nonzero__ for py2, |
15 |
- added BlockingIOError handling w/ py2 compat, |
16 |
- added unlinking of socket on server exit. |
17 |
--- |
18 |
bin/save-ebuild-env.sh | 5 +- |
19 |
bin/socks5-server.py | 233 +++++++++++++++++++++ |
20 |
.../package/ebuild/_config/special_env_vars.py | 2 +- |
21 |
pym/portage/package/ebuild/doebuild.py | 7 + |
22 |
pym/portage/util/socks5.py | 48 +++++ |
23 |
5 files changed, 292 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..45cf76b |
53 |
--- /dev/null |
54 |
+++ b/bin/socks5-server.py |
55 |
@@ -0,0 +1,233 @@ |
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 asyncore |
62 |
+import errno |
63 |
+import os |
64 |
+import socket |
65 |
+import struct |
66 |
+import sys |
67 |
+ |
68 |
+ |
69 |
+if sys.hexversion < 0x03000000: |
70 |
+ from io import BlockingIOError |
71 |
+ |
72 |
+ |
73 |
+class ProxyConnection(asyncore.dispatcher_with_send): |
74 |
+ _addr = None |
75 |
+ _connected = False |
76 |
+ _family = socket.AF_INET |
77 |
+ _proxy_conn = None |
78 |
+ |
79 |
+ def __init__(self, proxy_conn): |
80 |
+ self._proxy_conn = proxy_conn |
81 |
+ asyncore.dispatcher_with_send.__init__(self) |
82 |
+ self.create_socket(self._family, socket.SOCK_STREAM) |
83 |
+ |
84 |
+ def start_connection(self, host, port): |
85 |
+ try: |
86 |
+ self.connect((host, port)) |
87 |
+ except: |
88 |
+ self.handle_error() |
89 |
+ |
90 |
+ def handle_read(self): |
91 |
+ try: |
92 |
+ buf = self.recv(4096) |
93 |
+ except BlockingIOError: |
94 |
+ return |
95 |
+ self._proxy_conn.send(buf) |
96 |
+ |
97 |
+ def handle_connect(self): |
98 |
+ self._connected = True |
99 |
+ self._proxy_conn.send_connected(self._family, self.getsockname()) |
100 |
+ |
101 |
+ def handle_close(self): |
102 |
+ if self._connected: |
103 |
+ self._proxy_conn.remote_closed() |
104 |
+ |
105 |
+ def handle_error(self): |
106 |
+ e, v, tb = sys.exc_info() |
107 |
+ if isinstance(v, socket.gaierror) or isinstance(v, socket.herror): |
108 |
+ self.close() |
109 |
+ self._proxy_conn.send_failure(self._family, errno.EHOSTUNREACH) |
110 |
+ elif isinstance(e, OSError): |
111 |
+ self.close() |
112 |
+ self._proxy_conn.send_failure(self._family, v.errno) |
113 |
+ else: |
114 |
+ raise |
115 |
+ |
116 |
+ |
117 |
+class ProxyConnectionV6(ProxyConnection): |
118 |
+ _family = socket.AF_INET6 |
119 |
+ |
120 |
+ |
121 |
+class ProxyHandler(asyncore.dispatcher_with_send): |
122 |
+ _my_buf = b'' |
123 |
+ _my_conn = None |
124 |
+ _my_state = 0 |
125 |
+ _my_addr = None |
126 |
+ |
127 |
+ def handle_read(self): |
128 |
+ try: |
129 |
+ rd = self.recv(4096) |
130 |
+ except BlockingIOError: |
131 |
+ return |
132 |
+ |
133 |
+ self._my_buf += rd |
134 |
+ if self._my_state == 0: # waiting for hello |
135 |
+ if len(self._my_buf) >= 3: |
136 |
+ vers, method_no = struct.unpack('!BB', self._my_buf[:2]) |
137 |
+ if vers != 0x05: |
138 |
+ self.close() |
139 |
+ return |
140 |
+ if len(self._my_buf) >= 2 + method_no: |
141 |
+ for method in self._my_buf[2:2+method_no]: |
142 |
+ if method == 0x00: |
143 |
+ break |
144 |
+ else: |
145 |
+ # no supported method |
146 |
+ method = 0xFF |
147 |
+ |
148 |
+ repl = struct.pack('!BB', 0x05, method) |
149 |
+ self.send(repl) |
150 |
+ if method == 0xFF: |
151 |
+ self.close() |
152 |
+ return |
153 |
+ else: |
154 |
+ self._my_buf = self._my_buf[2+method_no:] |
155 |
+ self._my_state = 1 |
156 |
+ |
157 |
+ if self._my_state == 1: # waiting for request |
158 |
+ if len(self._my_buf) >= 5: |
159 |
+ vers, cmd, rsv, atyp = struct.unpack('!BBBB', self._my_buf[:4]) |
160 |
+ if vers != 0x05 or rsv != 0x00: |
161 |
+ self.close() |
162 |
+ return |
163 |
+ |
164 |
+ rpl = 0x00 |
165 |
+ addr_len = 0 |
166 |
+ if cmd != 0x01: # CONNECT |
167 |
+ rpl = 0x07 # command not supported |
168 |
+ elif atyp == 0x01: # IPv4 |
169 |
+ addr_len = 4 |
170 |
+ elif atyp == 0x03: # domain name |
171 |
+ addr_len, = struct.unpack('!B', self._my_buf[4:5]) |
172 |
+ addr_len += 1 # length field |
173 |
+ elif atyp == 0x04: # IPv6 |
174 |
+ addr_len = 16 |
175 |
+ else: |
176 |
+ rpl = 0x08 # address type not supported |
177 |
+ |
178 |
+ # terminate early |
179 |
+ if rpl != 0x00: |
180 |
+ repl = struct.pack('!BBBBLH', 0x05, rpl, 0x00, 0x01, |
181 |
+ 0x00000000, 0x0000) |
182 |
+ self.send(repl) |
183 |
+ self.close() |
184 |
+ return |
185 |
+ |
186 |
+ if len(self._my_buf) >= 6 + addr_len: |
187 |
+ if atyp == 0x01: |
188 |
+ addr = socket.inet_ntoa(self._my_buf[5:5+addr_len]) |
189 |
+ elif atyp == 0x03: |
190 |
+ addr = self._my_buf[5:4+addr_len] |
191 |
+ elif atyp == 0x04: |
192 |
+ addr = socket.inet_ntop(socket.AF_INET6, self._my_buf[5:5+addr_len]) |
193 |
+ port, = struct.unpack('!H', self._my_buf[4+addr_len:6+addr_len]) |
194 |
+ |
195 |
+ self._my_buf = self._my_buf[6+addr_len:] |
196 |
+ self._my_state = 2 |
197 |
+ self._my_addr = (atyp, addr, port) |
198 |
+ if atyp in (0x03, 0x04): |
199 |
+ # try IPv6 first |
200 |
+ self._my_conn = ProxyConnectionV6(self) |
201 |
+ else: |
202 |
+ self._my_conn = ProxyConnection(self) |
203 |
+ self._my_conn.start_connection(addr, port) |
204 |
+ |
205 |
+ if self._my_state == 2: # connecting |
206 |
+ pass |
207 |
+ |
208 |
+ if self._my_state == 3: # connected |
209 |
+ self._my_conn.send(self._my_buf) |
210 |
+ self._my_buf = b'' |
211 |
+ |
212 |
+ def handle_close(self): |
213 |
+ if self._my_conn is not None: |
214 |
+ self._my_conn.close() |
215 |
+ |
216 |
+ def send_connected(self, family, addr): |
217 |
+ |
218 |
+ if family == socket.AF_INET: |
219 |
+ host, port = addr |
220 |
+ bin_host = socket.inet_aton(host) |
221 |
+ |
222 |
+ repl = struct.pack('!BBBB4sH', 0x05, 0x00, 0x00, 0x01, |
223 |
+ bin_host, port) |
224 |
+ elif family == socket.AF_INET6: |
225 |
+ # discard flowinfo, scope_id |
226 |
+ host, port = addr[:2] |
227 |
+ bin_host = socket.inet_pton(family, host) |
228 |
+ |
229 |
+ repl = struct.pack('!BBBB16sH', 0x05, 0x00, 0x00, 0x04, |
230 |
+ bin_host, port) |
231 |
+ |
232 |
+ self.send(repl) |
233 |
+ self._my_state = 3 |
234 |
+ |
235 |
+ # flush the buffer |
236 |
+ if self._my_buf: |
237 |
+ self.handle_read() |
238 |
+ |
239 |
+ def send_failure(self, family, err): |
240 |
+ if family == socket.AF_INET6 and self._my_addr[0] != 0x04: |
241 |
+ # retry as IPv4 |
242 |
+ self._my_conn = ProxyConnection(self) |
243 |
+ self._my_conn.start_connection(*self._my_addr[1:]) |
244 |
+ return |
245 |
+ |
246 |
+ rpl = 0x01 # general error |
247 |
+ if err in (errno.ENETUNREACH, errno.ENETDOWN): |
248 |
+ rpl = 0x03 # network unreachable |
249 |
+ elif err in (errno.EHOSTUNREACH, errno.EHOSTDOWN): |
250 |
+ rpl = 0x04 # host unreachable |
251 |
+ elif err in (errno.ECONNREFUSED, errno.ETIMEDOUT): |
252 |
+ rpl = 0x05 # connection refused |
253 |
+ |
254 |
+ repl = struct.pack('!BBBBLH', 0x05, rpl, 0x00, 0x01, |
255 |
+ 0x00000000, 0x0000) |
256 |
+ self.send(repl) |
257 |
+ self.close() |
258 |
+ |
259 |
+ def remote_closed(self): |
260 |
+ self.close() |
261 |
+ |
262 |
+ |
263 |
+class ProxyServer(asyncore.dispatcher): |
264 |
+ def __init__(self, socket_path): |
265 |
+ asyncore.dispatcher.__init__(self) |
266 |
+ self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) |
267 |
+ self.set_reuse_addr() |
268 |
+ self.bind(socket_path) |
269 |
+ self.listen(5) |
270 |
+ |
271 |
+ def handle_accepted(self, sock, addr): |
272 |
+ h = ProxyHandler(sock) |
273 |
+ |
274 |
+ |
275 |
+if __name__ == '__main__': |
276 |
+ if len(sys.argv) != 2: |
277 |
+ print('Usage: %s <socket-path>' % sys.argv[0]) |
278 |
+ sys.exit(1) |
279 |
+ |
280 |
+ try: |
281 |
+ s = ProxyServer(sys.argv[1]) |
282 |
+ asyncore.loop() |
283 |
+ except KeyboardInterrupt: |
284 |
+ try: |
285 |
+ os.unlink(sys.argv[1]) |
286 |
+ except OSError: |
287 |
+ pass |
288 |
+ sys.exit(0) |
289 |
diff --git a/pym/portage/package/ebuild/_config/special_env_vars.py b/pym/portage/package/ebuild/_config/special_env_vars.py |
290 |
index 6bb3c95..905d5e7 100644 |
291 |
--- a/pym/portage/package/ebuild/_config/special_env_vars.py |
292 |
+++ b/pym/portage/package/ebuild/_config/special_env_vars.py |
293 |
@@ -71,7 +71,7 @@ environ_whitelist += [ |
294 |
"PORTAGE_PYM_PATH", "PORTAGE_PYTHON", |
295 |
"PORTAGE_PYTHONPATH", "PORTAGE_QUIET", |
296 |
"PORTAGE_REPO_NAME", "PORTAGE_REPOSITORIES", "PORTAGE_RESTRICT", |
297 |
- "PORTAGE_SIGPIPE_STATUS", |
298 |
+ "PORTAGE_SIGPIPE_STATUS", "PORTAGE_SOCKS5_PROXY", |
299 |
"PORTAGE_TMPDIR", "PORTAGE_UPDATE_ENV", "PORTAGE_USERNAME", |
300 |
"PORTAGE_VERBOSE", "PORTAGE_WORKDIR_MODE", "PORTAGE_XATTR_EXCLUDE", |
301 |
"PORTDIR", "PORTDIR_OVERLAY", "PREROOTPATH", |
302 |
diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py |
303 |
index e58181b..050f6c4 100644 |
304 |
--- a/pym/portage/package/ebuild/doebuild.py |
305 |
+++ b/pym/portage/package/ebuild/doebuild.py |
306 |
@@ -68,6 +68,7 @@ from portage.util import apply_recursive_permissions, \ |
307 |
writemsg, writemsg_stdout, write_atomic |
308 |
from portage.util.cpuinfo import get_cpu_count |
309 |
from portage.util.lafilefixer import rewrite_lafile |
310 |
+from portage.util.socks5 import get_socks5_proxy |
311 |
from portage.versions import _pkgsplit |
312 |
from _emerge.BinpkgEnvExtractor import BinpkgEnvExtractor |
313 |
from _emerge.EbuildBuildDir import EbuildBuildDir |
314 |
@@ -1487,6 +1488,12 @@ def spawn(mystring, mysettings, debug=False, free=False, droppriv=False, |
315 |
keywords['unshare_net'] = not networked |
316 |
keywords['unshare_ipc'] = not ipc |
317 |
|
318 |
+ if not networked: |
319 |
+ # Provide a SOCKS5-over-UNIX-socket proxy to escape sandbox |
320 |
+ proxy = get_socks5_proxy(mysettings) |
321 |
+ mysettings['PORTAGE_SOCKS5_PROXY'] = proxy |
322 |
+ mysettings['DISTCC_SOCKS_PROXY'] = proxy |
323 |
+ |
324 |
# TODO: Enable fakeroot to be used together with droppriv. The |
325 |
# fake ownership/permissions will have to be converted to real |
326 |
# permissions in the merge phase. |
327 |
diff --git a/pym/portage/util/socks5.py b/pym/portage/util/socks5.py |
328 |
new file mode 100644 |
329 |
index 0000000..ea0d2c1 |
330 |
--- /dev/null |
331 |
+++ b/pym/portage/util/socks5.py |
332 |
@@ -0,0 +1,48 @@ |
333 |
+# SOCKSv5 proxy manager for network-sandbox |
334 |
+# Copyright 2015 Gentoo Foundation |
335 |
+# Distributed under the terms of the GNU General Public License v2 |
336 |
+ |
337 |
+import os |
338 |
+import signal |
339 |
+ |
340 |
+from portage import _python_interpreter |
341 |
+from portage.data import portage_gid, portage_uid, userpriv_groups |
342 |
+from portage.process import atexit_register, spawn |
343 |
+ |
344 |
+ |
345 |
+class ProxyManager(object): |
346 |
+ socket_path = None |
347 |
+ _pids = None |
348 |
+ |
349 |
+ def start(self, settings): |
350 |
+ self.socket_path = os.path.join(settings['PORTAGE_TMPDIR'], |
351 |
+ '.portage.%d.net.sock' % os.getpid()) |
352 |
+ server_bin = os.path.join(settings['PORTAGE_BIN_PATH'], 'socks5-server.py') |
353 |
+ self._pids = spawn([_python_interpreter, server_bin, self.socket_path], |
354 |
+ returnpid=True, uid=portage_uid, gid=portage_gid, |
355 |
+ groups=userpriv_groups, umask=0o077) |
356 |
+ |
357 |
+ atexit_register(self.stop) |
358 |
+ |
359 |
+ def stop(self): |
360 |
+ self.socket_path = None |
361 |
+ |
362 |
+ for p in self._pids: |
363 |
+ os.kill(p, signal.SIGINT) |
364 |
+ os.waitpid(p, 0) |
365 |
+ |
366 |
+ def __bool__(self): |
367 |
+ return self.socket_path is not None |
368 |
+ |
369 |
+ def __nonzero__(self): |
370 |
+ return self.__bool__() |
371 |
+ |
372 |
+ |
373 |
+running_proxy = ProxyManager() |
374 |
+ |
375 |
+ |
376 |
+def get_socks5_proxy(settings): |
377 |
+ if not running_proxy: |
378 |
+ running_proxy.start(settings) |
379 |
+ |
380 |
+ return running_proxy.socket_path |
381 |
-- |
382 |
2.2.2 |