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

Replies