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 v4] Support escaping network-sandbox through SOCKSv5 proxy
Date: Sat, 31 Jan 2015 18:25:42
Message-Id: 1422728729-18396-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 | 190 +++++++++++++++++++++
18 .../package/ebuild/_config/special_env_vars.py | 2 +-
19 pym/portage/package/ebuild/doebuild.py | 11 ++
20 pym/portage/util/socks5.py | 53 ++++++
21 5 files changed, 258 insertions(+), 3 deletions(-)
22 create mode 100644 bin/socks5-server.py
23 create mode 100644 pym/portage/util/socks5.py
24
25 diff --git a/bin/save-ebuild-env.sh b/bin/save-ebuild-env.sh
26 index c6bffb5..477ed28 100644
27 --- a/bin/save-ebuild-env.sh
28 +++ b/bin/save-ebuild-env.sh
29 @@ -92,7 +92,7 @@ __save_ebuild_env() {
30
31 # portage config variables and variables set directly by portage
32 unset ACCEPT_LICENSE BAD BRACKET BUILD_PREFIX COLS \
33 - DISTCC_DIR DISTDIR DOC_SYMLINKS_DIR \
34 + DISTCC_DIR DISTCC_SOCKS5_PROXY DISTDIR DOC_SYMLINKS_DIR \
35 EBUILD_FORCE_TEST EBUILD_MASTER_PID \
36 ECLASS_DEPTH ENDCOL FAKEROOTKEY \
37 GOOD HILITE HOME \
38 @@ -105,7 +105,8 @@ __save_ebuild_env() {
39 PORTAGE_DOHTML_WARN_ON_SKIPPED_FILES \
40 PORTAGE_NONFATAL PORTAGE_QUIET \
41 PORTAGE_SANDBOX_DENY PORTAGE_SANDBOX_PREDICT \
42 - PORTAGE_SANDBOX_READ PORTAGE_SANDBOX_WRITE PREROOTPATH \
43 + PORTAGE_SANDBOX_READ PORTAGE_SANDBOX_WRITE \
44 + PORTAGE_SOCKS5_PROXY PREROOTPATH \
45 QA_INTERCEPTORS \
46 RC_DEFAULT_INDENT RC_DOT_PATTERN RC_ENDCOL RC_INDENTATION \
47 ROOT ROOTPATH RPMDIR TEMP TMP TMPDIR USE_EXPAND \
48 diff --git a/bin/socks5-server.py b/bin/socks5-server.py
49 new file mode 100644
50 index 0000000..6bd71e4
51 --- /dev/null
52 +++ b/bin/socks5-server.py
53 @@ -0,0 +1,190 @@
54 +#!/usr/bin/env python
55 +# SOCKSv5 proxy server for network-sandbox
56 +# Copyright 2015 Gentoo Foundation
57 +# Distributed under the terms of the GNU General Public License v2
58 +
59 +import asyncio
60 +import errno
61 +import os
62 +import socket
63 +import struct
64 +import sys
65 +
66 +class Socks5Server(object):
67 + @asyncio.coroutine
68 + def handle_proxy_conn(self, reader, writer):
69 + try:
70 + # SOCKS hello
71 + data = yield from reader.readexactly(2)
72 + vers, method_no = struct.unpack('!BB', data)
73 +
74 + if vers != 0x05:
75 + writer.close()
76 + return
77 +
78 + # ...and auth method list
79 + data = yield from reader.readexactly(method_no)
80 + for method in data:
81 + if method == 0x00:
82 + break
83 + else:
84 + # no supported method
85 + method = 0xFF
86 +
87 + # auth reply
88 + repl = struct.pack('!BB', 0x05, method)
89 + writer.write(repl)
90 + yield from writer.drain()
91 + if method == 0xFF:
92 + writer.close()
93 + return
94 +
95 + # request
96 + data = yield from reader.readexactly(4)
97 + vers, cmd, rsv, atyp = struct.unpack('!BBBB', data)
98 +
99 + if vers != 0x05 or rsv != 0x00:
100 + self.close()
101 + return
102 +
103 + rpl = 0x00
104 + if cmd != 0x01: # CONNECT
105 + rpl = 0x07 # command not supported
106 + elif atyp == 0x01: # IPv4
107 + data = yield from reader.readexactly(4)
108 + addr = socket.inet_ntoa(data)
109 + elif atyp == 0x03: # domain name
110 + data = yield from reader.readexactly(1)
111 + addr_len, = struct.unpack('!B', data)
112 + addr = yield from reader.readexactly(addr_len)
113 + elif atyp == 0x04: # IPv6
114 + data = yield from reader.readexactly(16)
115 + addr = socket.inet_ntop(socket.AF_INET6, data)
116 + else:
117 + rpl = 0x08 # address type not supported
118 +
119 + repl_addr = None
120 +
121 + # try to connect
122 + if rpl == 0x00:
123 + data = yield from reader.readexactly(2)
124 + port, = struct.unpack('!H', data)
125 +
126 + try:
127 + # establish proxied connection
128 + proxied_reader, proxied_writer = yield from asyncio.open_connection(
129 + addr, port)
130 + except (socket.gaierror, socket.herror):
131 + rpl = 0x04 # host unreachable
132 + except OSError as e:
133 + if e.errno in (errno.ENETUNREACH, errno.ENETDOWN):
134 + rpl = 0x03 # network unreachable
135 + elif e.errno in (errno.EHOSTUNREACH, errno.EHOSTDOWN):
136 + rpl = 0x04 # host unreachable
137 + elif e.errno in (errno.ECONNREFUSED, errno.ETIMEDOUT):
138 + rpl = 0x05 # connection refused
139 + else:
140 + raise
141 + else:
142 + sock = proxied_writer.get_extra_info('socket')
143 + addr = sock.getsockname()
144 + if sock.family == socket.AF_INET:
145 + host, port = addr
146 + bin_host = socket.inet_aton(host)
147 +
148 + repl_addr = struct.pack('!B4sH',
149 + 0x01, bin_host, port)
150 + elif sock.family == socket.AF_INET6:
151 + # discard flowinfo, scope_id
152 + host, port = addr[:2]
153 + bin_host = socket.inet_pton(sock.family, host)
154 +
155 + repl_addr = struct.pack('!B16sH',
156 + 0x04, bin_host, port)
157 +
158 + if repl_addr is None:
159 + # fallback to 0.0.0.0:0
160 + repl_addr = struct.pack('!BLH', 0x01, 0x00000000, 0x0000)
161 +
162 + # reply to the request
163 + repl = struct.pack('!BBB', 0x05, rpl, 0x00)
164 + writer.write(repl + repl_addr)
165 + yield from writer.drain()
166 +
167 + # close if an error occured
168 + if rpl != 0x00:
169 + writer.close()
170 + return
171 +
172 + # otherwise, start two loops:
173 + # remote -> local...
174 + t = asyncio.async(self.handle_proxied_conn(
175 + proxied_reader, writer, asyncio.Task.current_task()))
176 +
177 + # and local -> remote...
178 + try:
179 + try:
180 + while True:
181 + data = yield from reader.read(4096)
182 + if data == b'':
183 + t.cancel()
184 + break
185 +
186 + proxied_writer.write(data)
187 + yield from proxied_writer.drain()
188 + except OSError:
189 + t.cancel()
190 + except:
191 + t.cancel()
192 + raise
193 + finally:
194 + proxied_writer.close()
195 + writer.close()
196 +
197 + except (OSError, asyncio.IncompleteReadError, asyncio.CancelledError):
198 + writer.close()
199 + return
200 + except:
201 + writer.close()
202 + raise
203 +
204 + @asyncio.coroutine
205 + def handle_proxied_conn(self, proxied_reader, writer, parent_task):
206 + try:
207 + try:
208 + while True:
209 + data = yield from proxied_reader.read(4096)
210 + if data == b'':
211 + break
212 +
213 + writer.write(data)
214 + yield from writer.drain()
215 + finally:
216 + parent_task.cancel()
217 + except (OSError, asyncio.CancelledError):
218 + return
219 +
220 +
221 +if __name__ == '__main__':
222 + if len(sys.argv) != 2:
223 + print('Usage: %s <socket-path>' % sys.argv[0])
224 + sys.exit(1)
225 +
226 + loop = asyncio.get_event_loop()
227 + s = Socks5Server()
228 + server = loop.run_until_complete(
229 + asyncio.start_unix_server(s.handle_proxy_conn, sys.argv[1], loop=loop))
230 +
231 + ret = 0
232 + try:
233 + try:
234 + loop.run_forever()
235 + except KeyboardInterrupt:
236 + pass
237 + except:
238 + ret = 1
239 + finally:
240 + server.close()
241 + loop.run_until_complete(server.wait_closed())
242 + loop.close()
243 + os.unlink(sys.argv[1])
244 diff --git a/pym/portage/package/ebuild/_config/special_env_vars.py b/pym/portage/package/ebuild/_config/special_env_vars.py
245 index 6bb3c95..905d5e7 100644
246 --- a/pym/portage/package/ebuild/_config/special_env_vars.py
247 +++ b/pym/portage/package/ebuild/_config/special_env_vars.py
248 @@ -71,7 +71,7 @@ environ_whitelist += [
249 "PORTAGE_PYM_PATH", "PORTAGE_PYTHON",
250 "PORTAGE_PYTHONPATH", "PORTAGE_QUIET",
251 "PORTAGE_REPO_NAME", "PORTAGE_REPOSITORIES", "PORTAGE_RESTRICT",
252 - "PORTAGE_SIGPIPE_STATUS",
253 + "PORTAGE_SIGPIPE_STATUS", "PORTAGE_SOCKS5_PROXY",
254 "PORTAGE_TMPDIR", "PORTAGE_UPDATE_ENV", "PORTAGE_USERNAME",
255 "PORTAGE_VERBOSE", "PORTAGE_WORKDIR_MODE", "PORTAGE_XATTR_EXCLUDE",
256 "PORTDIR", "PORTDIR_OVERLAY", "PREROOTPATH",
257 diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py
258 index 791b5c3..6329b62 100644
259 --- a/pym/portage/package/ebuild/doebuild.py
260 +++ b/pym/portage/package/ebuild/doebuild.py
261 @@ -68,6 +68,7 @@ from portage.util import apply_recursive_permissions, \
262 writemsg, writemsg_stdout, write_atomic
263 from portage.util.cpuinfo import get_cpu_count
264 from portage.util.lafilefixer import rewrite_lafile
265 +from portage.util.socks5 import get_socks5_proxy
266 from portage.versions import _pkgsplit
267 from _emerge.BinpkgEnvExtractor import BinpkgEnvExtractor
268 from _emerge.EbuildBuildDir import EbuildBuildDir
269 @@ -1487,6 +1488,16 @@ def spawn(mystring, mysettings, debug=False, free=False, droppriv=False,
270 keywords['unshare_net'] = not networked
271 keywords['unshare_ipc'] = not ipc
272
273 + if not networked:
274 + # Provide a SOCKS5-over-UNIX-socket proxy to escape sandbox
275 + try:
276 + proxy = get_socks5_proxy(mysettings)
277 + except NotImplementedError:
278 + pass
279 + else:
280 + mysettings['PORTAGE_SOCKS5_PROXY'] = proxy
281 + mysettings['DISTCC_SOCKS_PROXY'] = proxy
282 +
283 # TODO: Enable fakeroot to be used together with droppriv. The
284 # fake ownership/permissions will have to be converted to real
285 # permissions in the merge phase.
286 diff --git a/pym/portage/util/socks5.py b/pym/portage/util/socks5.py
287 new file mode 100644
288 index 0000000..286cccf
289 --- /dev/null
290 +++ b/pym/portage/util/socks5.py
291 @@ -0,0 +1,53 @@
292 +# SOCKSv5 proxy manager for network-sandbox
293 +# Copyright 2015 Gentoo Foundation
294 +# Distributed under the terms of the GNU General Public License v2
295 +
296 +import os
297 +import signal
298 +
299 +from portage import _python_interpreter
300 +from portage.data import portage_gid, portage_uid, userpriv_groups
301 +from portage.process import atexit_register, spawn
302 +
303 +
304 +class ProxyManager(object):
305 + socket_path = None
306 + _pids = None
307 +
308 + def start(self, settings):
309 + try:
310 + import asyncio
311 + except ImportError:
312 + raise NotImplementedError('SOCKSv5 proxy requires asyncio module')
313 +
314 + self.socket_path = os.path.join(settings['PORTAGE_TMPDIR'],
315 + '.portage.%d.net.sock' % os.getpid())
316 + server_bin = os.path.join(settings['PORTAGE_BIN_PATH'], 'socks5-server.py')
317 + self._pids = spawn([_python_interpreter, server_bin, self.socket_path],
318 + returnpid=True, uid=portage_uid, gid=portage_gid,
319 + groups=userpriv_groups, umask=0o077)
320 +
321 + atexit_register(self.stop)
322 +
323 + def stop(self):
324 + self.socket_path = None
325 +
326 + for p in self._pids:
327 + os.kill(p, signal.SIGINT)
328 + os.waitpid(p, 0)
329 +
330 + def __bool__(self):
331 + return self.socket_path is not None
332 +
333 + def __nonzero__(self):
334 + return self.__bool__()
335 +
336 +
337 +running_proxy = ProxyManager()
338 +
339 +
340 +def get_socks5_proxy(settings):
341 + if not running_proxy:
342 + running_proxy.start(settings)
343 +
344 + return running_proxy.socket_path
345 --
346 2.2.2