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 v3] Support escaping network-sandbox through SOCKSv5 proxy
Date: Sun, 25 Jan 2015 22:34:16
Message-Id: 1422225246-26223-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 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

Replies