5b1dbd89cdb0a8d7572fbc4470972a5646e9ddc4
[proxyspace.git] / proxyspace.c
1 // compile with gcc -o proxyspace proxyspace.c -Wall -std=gnu99 -lbsd
2
3 // general note: Yeah, I don't always free stuff. Doesn't run in loops though,
4 // so it's not more than a KB or so that's wasted. Who cares.
5
6 #define _GNU_SOURCE
7
8 #include <netdb.h>
9 #include <sys/types.h>
10 #include <sys/socket.h>
11 #include <sys/wait.h>
12 #include <netinet/in.h>
13 #include <netinet/tcp.h>
14 #include <unistd.h>
15 #include <err.h>
16 #include <stdio.h>
17 #include <sched.h>
18 #include <sys/stat.h>
19 #include <fcntl.h>
20 #include <string.h>
21 #include <stdarg.h>
22 #include <alloca.h>
23 #include <stdlib.h>
24 #include <linux/un.h>
25 #include <bsd/string.h>
26 #include <sys/mount.h>
27
28 #define eprintf(...) fprintf(stderr, __VA_ARGS__)
29
30 const struct addrinfo tcp_hints = {
31   .ai_flags = AI_ADDRCONFIG,
32   .ai_family = AF_UNSPEC,
33   .ai_socktype = SOCK_STREAM,
34   .ai_protocol = 0
35 };
36
37 int netopen(const char *node, const char *service) {
38   struct addrinfo *addrs;
39   if (getaddrinfo(node, service, &tcp_hints, &addrs))
40     errx(1, "unable to resolve address");
41
42   int s = socket(addrs[0].ai_family, addrs[0].ai_socktype, addrs[0].ai_protocol);
43   if (s == -1)
44     err(1, "unable to create socket");
45
46   if (connect(s, addrs[0].ai_addr, addrs[0].ai_addrlen))
47     err(1, "unable to connect");
48
49   freeaddrinfo(addrs);
50   return s;
51 }
52
53 pid_t fork_nofail(void) {
54   pid_t res = fork();
55   if (res == -1)
56     err(1, "fork failed");
57   return res;
58 }
59
60 void write_file(char *path, char *data) {
61   int fd = open(path, O_WRONLY);
62   if (fd == -1)
63     err(1, "unable to open \"%s\" for writing", path);
64   ssize_t r = write(fd, data, strlen(data));
65   if (r == -1)
66     err(1, "write to \"%s\" failed", path);
67   if (r != strlen(data))
68     errx(1, "write to \"%s\" failed", path);
69   if (close(fd))
70     err(1, "close failed");
71 }
72
73 char *xasprintf(char *fmt, ...) {
74   va_list ap;
75   va_start(ap, fmt);
76   char *ret;
77   if (vasprintf(&ret, fmt, ap) == -1)
78     errx(1, "memory allocation failed");
79   va_end(ap);
80   return ret;
81 }
82
83 char *randhex(void) {
84   char *ret = malloc(33);
85   if (!ret)
86     errx(1, "unable to malloc");
87   int fd = open("/dev/urandom", O_RDONLY);
88   if (fd == -1)
89     err(1, "unable to open urandom");
90   if (read(fd, ret, 32) != 32)
91     errx(1, "read from urandom failed");
92   close(fd);
93   for (int i=0; i<32; i++) {
94     ret[i] = (ret[i] & 0xf) + 'A';
95   }
96   ret[32] = 0;
97   return ret;
98 }
99
100 int recvfd(int sock) {
101   int len = sizeof(struct cmsghdr) + sizeof(int);
102   struct cmsghdr *hdr = alloca(len);
103   char ch = '\0';
104   struct iovec vec = {
105     .iov_base = &ch,
106     .iov_len = 1
107   };
108   struct msghdr msg = {
109     .msg_control = hdr,
110     .msg_controllen = len,
111     .msg_iov = &vec,
112     .msg_iovlen = 1
113   };
114   if (recvmsg(sock, &msg, 0) < 0)
115     err(1, "unable to receive fd");
116   if (hdr->cmsg_len != len || hdr->cmsg_level != SOL_SOCKET || hdr->cmsg_type != SCM_RIGHTS)
117     errx(1, "got bad message");
118   return *(int*)CMSG_DATA(hdr);
119 }
120
121 void sendfd(int sock, int fd) {
122   int len = sizeof(struct cmsghdr) + sizeof(int);
123   struct cmsghdr *hdr = alloca(len);
124   *hdr = (struct cmsghdr) {
125     .cmsg_len = len,
126     .cmsg_level = SOL_SOCKET,
127     .cmsg_type = SCM_RIGHTS
128   };
129   *(int*)CMSG_DATA(hdr) = fd;
130   char ch = '\0';
131   struct iovec vec = {
132     .iov_base = &ch,
133     .iov_len = 1
134   };
135   struct msghdr msg = {
136     .msg_control = hdr,
137     .msg_controllen = len,
138     .msg_iov = &vec,
139     .msg_iovlen = 1
140   };
141   if (sendmsg(sock, &msg, 0) < 0)
142     err(1, "unable to send fd");
143 }
144
145 int main(int argc, char **argv) {
146   if (!strcmp(argv[1], "--stage2")) {
147     eprintf("stage2 helper running...\n");
148     int usock = socket(AF_UNIX, SOCK_STREAM, 0);
149     if (usock == -1)
150       err(1, "unable to create unix sock");
151     struct sockaddr_un unix_addr = {
152       .sun_family = AF_UNIX
153     };
154     char *socket_path = getenv("PROXYSPACE_SOCKET_PATH");
155     if (!socket_path)
156       errx(1, "helper called without socket path in env");
157     if (strlcpy(unix_addr.sun_path, socket_path, UNIX_PATH_MAX) >= UNIX_PATH_MAX)
158       errx(1, "string is too long in helper");
159     if (connect(usock, (struct sockaddr *)&unix_addr, sizeof(unix_addr)))
160       err(1, "unable to connect to unix socket");
161
162     int proxyfd = recvfd(usock);
163     sendfd(1, proxyfd);
164     eprintf("sent proxy fd to ssh");
165     return 0;
166   }
167
168   // invocation: ./proxyspace <sshhost-tcp> <sshport-tcp> <sshtarget-name>
169   char *sshhost = argv[1];
170   char *sshport = argv[2];
171   char *sshtarget = argv[3];
172
173   eprintf("connecting...\n");
174   int netfd = netopen(sshhost, sshport);
175   eprintf("TCP connection successful\n");
176
177   uid_t outer_uid = getuid();
178   gid_t outer_gid = getgid();
179
180   // This is the point of no return: After this, we don't have access to the normal
181   // network anymore.
182   if (unshare(CLONE_NEWUSER|CLONE_NEWNET))
183     err(1, "unable to unshare (maybe unprivileged user namespaces are disabled)");
184
185   // Annoying. Fix up our mapped uid and gid to zero.
186   // On super-new kernels, we could use ambient capabilities here, that would avoid some trouble.
187   write_file("/proc/self/setgroups", "deny");
188   write_file("/proc/self/uid_map", xasprintf("0 %d 1\n", outer_uid));
189   write_file("/proc/self/gid_map", xasprintf("0 %d 1\n", outer_gid));
190
191   // we need to pass an FD to the helper through ssh, but ssh closes fds automatically.
192   // so we go for the ugly solution and pass the fd through unix domain sockets twice.
193   char *tempdir = getenv("XDG_RUNTIME_DIR");
194   if (!tempdir)
195     errx(1, "no XDG_RUNTIME_DIR set in the environment - please set it to a safe tempdir");
196   char *socket_path = xasprintf("%s/proxyspace.socket-%s", tempdir, randhex());
197   int usock = socket(AF_UNIX, SOCK_STREAM, 0);
198   if (usock == -1)
199     err(1, "unable to create unix sock");
200   struct sockaddr_un unix_addr = {
201     .sun_family = AF_UNIX
202   };
203   if (strlcpy(unix_addr.sun_path, socket_path, UNIX_PATH_MAX) >= UNIX_PATH_MAX)
204     errx(1, "stupid string is too long");
205   if (bind(usock, (struct sockaddr *)&unix_addr, sizeof(unix_addr)))
206     err(1, "unable to bind unix socket");
207   if (listen(usock, 5))
208     err(1, "unable to listen");
209
210   eprintf("launching ssh...\n");
211   pid_t ssh_pid = fork_nofail();
212   if (ssh_pid == 0) {
213     close(usock);
214     close(netfd);
215     setenv("PROXYSPACE_SOCKET_PATH", socket_path, 1);
216     // ssh is too smart here and determines the homedir based on the UID it sees (0)
217     // instead of looking at $HOME like everyone else. therefore *sigh* also make a
218     // new mount namespace while we're at it and then bind-mount our $HOME to /root.
219     if (unshare(CLONE_NEWNS))
220       err(1, "unable to unshare mount ns");
221     if (mount(xasprintf("%s/", getenv("HOME")), "/root", NULL, MS_BIND|MS_REC, NULL))
222       err(1, "unable to do the homedir fixup step");
223
224     execlp("ssh", "ssh", "-v", "-f", "-n", "-w", "0:42", // HACK hardcoded remote tunnel interface number (local is okay)
225       "-o", xasprintf("ProxyCommand /proc/%d/exe --stage2 %d", (int)getppid(), netfd),
226       "-o", "ProxyUseFdpass yes",
227       sshtarget,
228
229       /* really hacky, yes */
230       "ifconfig tun42 192.168.244.1 pointopoint 192.168.244.2 netmask 255.255.255.0 &&"
231       "while true; do sleep 60; done",
232
233       NULL);
234     err(1, "execlp for ssh failed");
235   }
236
237   int usock_c = accept(usock, NULL, NULL);
238   if (usock_c == -1)
239     err(1, "can't accept");
240   sendfd(usock_c, netfd);
241   close(usock_c);
242   close(usock);
243   eprintf("sent fd to helper.\n");
244
245   int ssh_status;
246   pid_t waitres;
247   do {
248     waitres = wait(&ssh_status);
249   } while (waitres != -1 && waitres != ssh_pid);
250   if (waitres == -1)
251     err(1, "unable to wait for ssh???");
252   if (!WIFEXITED(ssh_status))
253     errx(1, "uh... looks like ssh exited in a weird way?");
254   if (WEXITSTATUS(ssh_status) != 0)
255     errx(1, "ssh exited with exit code %d", (int)WEXITSTATUS(ssh_status));
256   eprintf("ssh seems to be connected!\n");
257   if (system("/sbin/ifconfig tun0 192.168.244.2 pointopoint 192.168.244.1 netmask 255.255.255.0"))
258     errx(1, "ifconfig failed");
259   if (system("ip route replace default via 192.168.244.1"))
260     errx(1, "route failed");
261
262   execlp("/bin/bash", "bash", NULL);
263   err(1, "unable to execute bash");
264 }