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] Support escaping network-sandbox through SOCKSv5 proxy
Date: Sun, 25 Jan 2015 11:30:04
Message-Id: 1422185394-6403-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 currently supports IPv4 only, and does not report bound
8 address (reports 0.0.0.0:0). No authentication is supported (UNIX
9 sockets provide a security layer).
10 ---
11 bin/save-ebuild-env.sh | 5 +-
12 bin/socks5-server.py | 171 +++++++++++++++++++++
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, 227 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..4795dcc
46 --- /dev/null
47 +++ b/bin/socks5-server.py
48 @@ -0,0 +1,171 @@
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 + _proxy_conn = None
63 +
64 + def __init__(self, host, port, proxy_conn):
65 + self._proxy_conn = proxy_conn
66 + asyncore.dispatcher_with_send.__init__(self)
67 + # TODO: how to support IPv6? ugly fail-then-reinit?
68 + self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
69 + self.connect((host, port))
70 +
71 + def handle_read(self):
72 + buf = self.recv(4096)
73 + self._proxy_conn.send(buf)
74 +
75 + def handle_connect(self):
76 + self._proxy_conn.send_connected()
77 +
78 + def handle_close(self):
79 + self._proxy_conn.close()
80 +
81 + def handle_error(self):
82 + e, v, tb = sys.exc_info()
83 + if e is OSError:
84 + self._proxy_conn.send_failure(v.errno)
85 + self.close()
86 + else:
87 + raise
88 +
89 +
90 +class ProxyHandler(asyncore.dispatcher_with_send):
91 + _my_buf = b''
92 + _my_conn = None
93 + _my_state = 0
94 +
95 + def handle_read(self):
96 + rd = self.recv(4096)
97 + if not rd:
98 + return
99 +
100 + self._my_buf += rd
101 + if self._my_state == 0: # waiting for hello
102 + if len(self._my_buf) >= 3:
103 + vers, method_no = struct.unpack('!BB', self._my_buf[:2])
104 + if vers != 0x05:
105 + self.close()
106 + return
107 + if len(self._my_buf) >= 2 + method_no:
108 + for method in self._my_buf[2:2+method_no]:
109 + if method == 0x00:
110 + break
111 + else:
112 + # no supported method
113 + method = 0xFF
114 +
115 + repl = struct.pack('!BB', 0x05, method)
116 + self.send(repl)
117 + if method == 0xFF:
118 + self.close()
119 + return
120 + else:
121 + self._my_buf = self._my_buf[2+method_no:]
122 + self._my_state = 1
123 +
124 + if self._my_state == 1: # waiting for request
125 + if len(self._my_buf) >= 5:
126 + vers, cmd, rsv, atyp = struct.unpack('!BBBB', self._my_buf[:4])
127 + if vers != 0x05 or rsv != 0x00:
128 + self.close()
129 + return
130 +
131 + rpl = 0x00
132 + addr_len = 0
133 + if cmd != 0x01: # CONNECT
134 + rpl = 0x07 # command not supported
135 + elif atyp == 0x01: # IPv4
136 + addr_len = 4
137 + elif atyp == 0x03: # domain name
138 + addr_len, = struct.unpack('!B', self._my_buf[4:5])
139 + addr_len += 1 # length field
140 +# elif atyp == 0x04: # IPv6
141 +# addr_len = 16
142 + else:
143 + rpl = 0x08 # address type not supported
144 +
145 + # terminate early
146 + if rpl != 0x00:
147 + repl = struct.pack('!BBBBLH', 0x05, rpl, 0x00, 0x01,
148 + 0x00000000, 0x0000)
149 + self.send(repl)
150 + self.close()
151 + return
152 +
153 + if len(self._my_buf) >= 6 + addr_len:
154 + if atyp == 0x01:
155 + addr = socket.inet_ntoa(self._my_buf[5:5+addr_len])
156 + elif atyp == 0x03:
157 + addr = self._my_buf[5:4+addr_len]
158 +# elif atyp == 0x04:
159 +# addr = socket.inet_ntop(socket.AF_INET6, self._my_buf[5:5+addr_len])
160 + port, = struct.unpack('!H', self._my_buf[4+addr_len:6+addr_len])
161 +
162 + self._my_buf = self._my_buf[6+addr_len:]
163 + self._my_state = 2
164 + self._my_conn = ProxyConnection(addr, port, self)
165 +
166 + if self._my_state == 2: # connecting
167 + pass
168 +
169 + if self._my_state == 3: # connected
170 + self._my_conn.send(self._my_buf)
171 + self._my_buf = b''
172 +
173 + def handle_close(self):
174 + if self._my_conn is not None:
175 + self._my_conn.close()
176 +
177 + def send_connected(self):
178 + repl = struct.pack('!BBBBLH', 0x05, 0x00, 0x00, 0x01,
179 + 0x00000000, 0x0000)
180 + self.send(repl)
181 + self._my_state = 3
182 +
183 + def send_failure(self, err):
184 + rpl = 0x01 # general error
185 + if err in (errno.ENETUNREACH, errno.ENETDOWN):
186 + rpl = 0x03 # network unreachable
187 + elif err in (errno.EHOSTUNREACH, errno.EHOSTDOWN):
188 + rpl = 0x04 # host unreachable
189 + elif err in (errno.ECONNREFUSED, errno.ETIMEDOUT):
190 + rpl = 0x05 # connection refused
191 +
192 + repl = struct.pack('!BBBBLH', 0x05, rpl, 0x00, 0x01,
193 + 0x00000000, 0x0000)
194 + self.send(repl)
195 + self.close()
196 +
197 +
198 +class ProxyServer(asyncore.dispatcher):
199 + def __init__(self, socket_path):
200 + asyncore.dispatcher.__init__(self)
201 + self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
202 + self.set_reuse_addr()
203 + self.bind(socket_path)
204 + self.listen(5)
205 +
206 + def handle_accepted(self, sock, addr):
207 + h = ProxyHandler(sock)
208 +
209 +
210 +if __name__ == '__main__':
211 + if len(sys.argv) != 2:
212 + print('Usage: %s <socket-path>' % sys.argv[0])
213 + sys.exit(1)
214 +
215 + try:
216 + s = ProxyServer(sys.argv[1])
217 + asyncore.loop()
218 + except KeyboardInterrupt:
219 + sys.exit(0)
220 diff --git a/pym/portage/package/ebuild/_config/special_env_vars.py b/pym/portage/package/ebuild/_config/special_env_vars.py
221 index 6bb3c95..905d5e7 100644
222 --- a/pym/portage/package/ebuild/_config/special_env_vars.py
223 +++ b/pym/portage/package/ebuild/_config/special_env_vars.py
224 @@ -71,7 +71,7 @@ environ_whitelist += [
225 "PORTAGE_PYM_PATH", "PORTAGE_PYTHON",
226 "PORTAGE_PYTHONPATH", "PORTAGE_QUIET",
227 "PORTAGE_REPO_NAME", "PORTAGE_REPOSITORIES", "PORTAGE_RESTRICT",
228 - "PORTAGE_SIGPIPE_STATUS",
229 + "PORTAGE_SIGPIPE_STATUS", "PORTAGE_SOCKS5_PROXY",
230 "PORTAGE_TMPDIR", "PORTAGE_UPDATE_ENV", "PORTAGE_USERNAME",
231 "PORTAGE_VERBOSE", "PORTAGE_WORKDIR_MODE", "PORTAGE_XATTR_EXCLUDE",
232 "PORTDIR", "PORTDIR_OVERLAY", "PREROOTPATH",
233 diff --git a/pym/portage/package/ebuild/doebuild.py b/pym/portage/package/ebuild/doebuild.py
234 index 791b5c3..0d71f01 100644
235 --- a/pym/portage/package/ebuild/doebuild.py
236 +++ b/pym/portage/package/ebuild/doebuild.py
237 @@ -68,6 +68,7 @@ from portage.util import apply_recursive_permissions, \
238 writemsg, writemsg_stdout, write_atomic
239 from portage.util.cpuinfo import get_cpu_count
240 from portage.util.lafilefixer import rewrite_lafile
241 +from portage.util.socks5 import get_socks5_proxy
242 from portage.versions import _pkgsplit
243 from _emerge.BinpkgEnvExtractor import BinpkgEnvExtractor
244 from _emerge.EbuildBuildDir import EbuildBuildDir
245 @@ -1487,6 +1488,12 @@ def spawn(mystring, mysettings, debug=False, free=False, droppriv=False,
246 keywords['unshare_net'] = not networked
247 keywords['unshare_ipc'] = not ipc
248
249 + if not networked:
250 + # Provide a SOCKS5-over-UNIX-socket proxy to escape sandbox
251 + proxy = get_socks5_proxy(mysettings)
252 + mysettings['PORTAGE_SOCKS5_PROXY'] = proxy
253 + mysettings['DISTCC_SOCKS_PROXY'] = proxy
254 +
255 # TODO: Enable fakeroot to be used together with droppriv. The
256 # fake ownership/permissions will have to be converted to real
257 # permissions in the merge phase.
258 diff --git a/pym/portage/util/socks5.py b/pym/portage/util/socks5.py
259 new file mode 100644
260 index 0000000..c8b3d6a
261 --- /dev/null
262 +++ b/pym/portage/util/socks5.py
263 @@ -0,0 +1,45 @@
264 +# SOCKSv5 proxy manager for network-sandbox
265 +# Copyright 2015 Gentoo Foundation
266 +# Distributed under the terms of the GNU General Public License v2
267 +
268 +import os
269 +import signal
270 +
271 +from portage import _python_interpreter
272 +from portage.data import portage_gid, portage_uid, userpriv_groups
273 +from portage.process import atexit_register, spawn
274 +
275 +
276 +class ProxyManager(object):
277 + socket_path = None
278 + _pids = None
279 +
280 + def start(self, settings):
281 + self.socket_path = os.path.join(settings['PORTAGE_TMPDIR'],
282 + '.portage.%d.net.sock' % os.getpid())
283 + server_bin = os.path.join(settings['PORTAGE_BIN_PATH'], 'socks5-server.py')
284 + self._pids = spawn([_python_interpreter, server_bin, self.socket_path],
285 + returnpid=True, uid=portage_uid, gid=portage_gid,
286 + groups=userpriv_groups, umask=0o077)
287 +
288 + atexit_register(self.stop)
289 +
290 + def stop(self):
291 + self.socket_path = None
292 +
293 + for p in self._pids:
294 + os.kill(p, signal.SIGINT)
295 + os.waitpid(p, 0)
296 +
297 + def __bool__(self):
298 + return self.socket_path is not None
299 +
300 +
301 +running_proxy = ProxyManager()
302 +
303 +
304 +def get_socks5_proxy(settings):
305 + if not running_proxy:
306 + running_proxy.start(settings)
307 +
308 + return running_proxy.socket_path
309 --
310 2.2.2

Replies