add README
[rofl0r-microsocks.git] / sockssrv.c
blob754de07db7b05608c2d45e4e51c761fd8276fb8e
1 /*
2 MicroSocks - multithreaded, small, efficient SOCKS5 server.
4 Copyright (C) 2017 rofl0r.
6 This is the successor of "rocksocks5", and it was written with
7 different goals in mind:
9 - prefer usage of standard libc functions over homegrown ones
10 - no artificial limits
11 - do not aim for minimal binary size, but for minimal source code size,
12 and maximal readability, reusability, and extensibility.
14 as a result of that, ipv4, dns, and ipv6 is supported out of the box
15 and can use the same code, while rocksocks5 has several compile time
16 defines to bring down the size of the resulting binary to extreme values
17 like 10 KB static linked when only ipv4 support is enabled.
19 still, if optimized for size, *this* program when static linked against musl
20 libc is not even 50 KB. that's easily usable even on the cheapest routers.
24 #define _GNU_SOURCE
25 #include <unistd.h>
26 #include <stdlib.h>
27 #include <string.h>
28 #include <stdio.h>
29 #include <pthread.h>
30 #include <sys/select.h>
31 #include <arpa/inet.h>
32 #include <errno.h>
33 #include "server.h"
34 #include "sblist.h"
36 static const char* auth_user;
37 static const char* auth_pass;
38 static sblist* auth_ips;
39 static pthread_mutex_t auth_ips_mutex = PTHREAD_MUTEX_INITIALIZER;
41 enum socksstate {
42 SS_1_CONNECTED,
43 SS_2_NEED_AUTH, /* skipped if NO_AUTH method supported */
44 SS_3_AUTHED,
47 enum authmethod {
48 AM_NO_AUTH = 0,
49 AM_GSSAPI = 1,
50 AM_USERNAME = 2,
51 AM_INVALID = 0xFF
54 enum errorcode {
55 EC_SUCCESS = 0,
56 EC_GENERAL_FAILURE = 1,
57 EC_NOT_ALLOWED = 2,
58 EC_NET_UNREACHABLE = 3,
59 EC_HOST_UNREACHABLE = 4,
60 EC_CONN_REFUSED = 5,
61 EC_TTL_EXPIRED = 6,
62 EC_COMMAND_NOT_SUPPORTED = 7,
63 EC_ADDRESSTYPE_NOT_SUPPORTED = 8,
66 struct thread {
67 pthread_t pt;
68 struct client client;
69 enum socksstate state;
70 volatile int done;
73 #ifndef CONFIG_LOG
74 #define CONFIG_LOG 1
75 #endif
76 #if CONFIG_LOG
77 /* we log to stderr because it's not using line buffering, i.e. malloc which would need
78 locking when called from different threads. for the same reason we use dprintf,
79 which writes directly to an fd. */
80 #define dolog(...) dprintf(2, __VA_ARGS__)
81 #else
82 static void dolog(const char* fmt, ...) { }
83 #endif
85 static int connect_socks_target(unsigned char *buf, size_t n, struct client *client) {
86 if(n < 5) return -EC_GENERAL_FAILURE;
87 if(buf[0] != 5) return -EC_GENERAL_FAILURE;
88 if(buf[1] != 1) return -EC_COMMAND_NOT_SUPPORTED; /* we support only CONNECT method */
89 if(buf[2] != 0) return -EC_GENERAL_FAILURE; /* malformed packet */
91 int af = AF_INET;
92 size_t minlen = 4 + 4 + 2, l;
93 char namebuf[256];
94 struct addrinfo* remote;
96 switch(buf[3]) {
97 case 4: /* ipv6 */
98 af = AF_INET6;
99 minlen = 4 + 2 + 16;
100 /* fall through */
101 case 1: /* ipv4 */
102 if(n < minlen) return -EC_GENERAL_FAILURE;
103 if(namebuf != inet_ntop(af, buf+4, namebuf, sizeof namebuf))
104 return -EC_GENERAL_FAILURE; /* malformed or too long addr */
105 break;
106 case 3: /* dns name */
107 l = buf[4];
108 minlen = 4 + 2 + l + 1;
109 if(n < 4 + 2 + l + 1) return -EC_GENERAL_FAILURE;
110 memcpy(namebuf, buf+4+1, l);
111 namebuf[l] = 0;
112 break;
113 default:
114 return -EC_ADDRESSTYPE_NOT_SUPPORTED;
116 unsigned short port;
117 port = (buf[minlen-2] << 8) | buf[minlen-1];
118 if(resolve(namebuf, port, &remote)) return -9;
119 int fd = socket(remote->ai_addr->sa_family, SOCK_STREAM, 0);
120 if(fd == -1) {
121 eval_errno:
122 freeaddrinfo(remote);
123 switch(errno) {
124 case EPROTOTYPE:
125 case EPROTONOSUPPORT:
126 case EAFNOSUPPORT:
127 return -EC_ADDRESSTYPE_NOT_SUPPORTED;
128 case ECONNREFUSED:
129 return -EC_CONN_REFUSED;
130 case ENETDOWN:
131 case ENETUNREACH:
132 return -EC_NET_UNREACHABLE;
133 case EHOSTUNREACH:
134 return -EC_HOST_UNREACHABLE;
135 case EBADF:
136 default:
137 perror("socket/connect");
138 return -EC_GENERAL_FAILURE;
141 if(connect(fd, remote->ai_addr, remote->ai_addrlen) == -1)
142 goto eval_errno;
144 freeaddrinfo(remote);
145 if(CONFIG_LOG) {
146 char clientname[256];
147 af = client->addr.v4.sin_family;
148 void *ipdata = af == AF_INET ? (void*)&client->addr.v4.sin_addr : (void*)&client->addr.v6.sin6_addr;
149 inet_ntop(af, ipdata, clientname, sizeof clientname);
150 dolog("client[%d] %s: connected to %s:%d\n", client->fd, clientname, namebuf, port);
152 return fd;
155 static int is_authed(union sockaddr_union *client, union sockaddr_union *authedip) {
156 if(authedip->v4.sin_family == client->v4.sin_family) {
157 int af = authedip->v4.sin_family;
158 size_t cmpbytes = af == AF_INET ? 4 : 16;
159 void *cmp1 = af == AF_INET ? (void*)&client->v4.sin_addr : (void*)&client->v6.sin6_addr;
160 void *cmp2 = af == AF_INET ? (void*)&authedip->v4.sin_addr : (void*)&authedip->v6.sin6_addr;
161 if(!memcmp(cmp1, cmp2, cmpbytes)) return 1;
163 return 0;
166 static enum authmethod check_auth_method(unsigned char *buf, size_t n, struct client*client) {
167 if(buf[0] != 5) return AM_INVALID;
168 size_t idx = 1;
169 if(idx >= n ) return AM_INVALID;
170 int n_methods = buf[idx];
171 idx++;
172 while(idx < n) {
173 if(buf[idx] == AM_NO_AUTH) {
174 if(!auth_user) return AM_NO_AUTH;
175 else if(auth_ips) {
176 size_t i;
177 int authed = 0;
178 pthread_mutex_lock(&auth_ips_mutex);
179 for(i=0;i<sblist_getsize(auth_ips);i++) {
180 if((authed = is_authed(&client->addr, sblist_get(auth_ips, i))))
181 break;
183 pthread_mutex_unlock(&auth_ips_mutex);
184 if(authed) return AM_NO_AUTH;
186 } else if(buf[idx] == AM_USERNAME) {
187 if(auth_user) return AM_USERNAME;
189 idx++;
191 return AM_INVALID;
194 static void add_auth_ip(struct client*client) {
195 pthread_mutex_lock(&auth_ips_mutex);
196 sblist_add(auth_ips, &client->addr);
197 pthread_mutex_unlock(&auth_ips_mutex);
200 static void send_auth_response(int fd, enum authmethod meth) {
201 unsigned char buf[2];
202 buf[0] = 5;
203 buf[1] = meth;
204 write(fd, buf, 2);
207 static void send_error(int fd, enum errorcode ec) {
208 /* position 4 contains ATYP, the address type, which is the same as used in the connect
209 request. we're lazy and return always IPV4 address type in errors. */
210 char buf[10] = { 5, ec, 0, 1 /*AT_IPV4*/, 0,0,0,0, 0,0 };
211 write(fd, buf, 10);
214 static void copyloop(int fd1, int fd2) {
215 int maxfd = fd2;
216 if(fd1 > fd2) maxfd = fd1;
217 fd_set fdsc, fds;
218 FD_ZERO(&fdsc);
219 FD_SET(fd1, &fdsc);
220 FD_SET(fd2, &fdsc);
222 while(1) {
223 memcpy(&fds, &fdsc, sizeof(fds));
224 /* inactive connections are reaped after 15 min to free resources.
225 usually programs send keep-alive packets so this should only happen
226 when a connection is really unused. */
227 struct timeval timeout = {.tv_sec = 60*15, .tv_usec = 0};
228 switch(select(maxfd+1, &fds, 0, 0, &timeout)) {
229 case 0:
230 send_error(fd1, EC_TTL_EXPIRED);
231 return;
232 case -1:
233 if(errno = EINTR) continue;
234 else perror("select");
235 return;
237 int infd = FD_ISSET(fd1, &fds) ? fd1 : fd2;
238 int outfd = infd == fd2 ? fd1 : fd2;
239 char buf[1024];
240 ssize_t sent = 0, n = read(infd, buf, sizeof buf);
241 if(n <= 0) return;
242 while(sent < n) {
243 ssize_t m = write(outfd, buf+sent, n-sent);
244 if(m < 0) return;
245 sent += m;
250 static enum errorcode check_credentials(unsigned char* buf, size_t n) {
251 if(n < 5) return EC_GENERAL_FAILURE;
252 if(buf[0] != 1) return EC_GENERAL_FAILURE;
253 unsigned ulen, plen;
254 ulen=buf[1];
255 if(n < 2 + ulen + 2) return EC_GENERAL_FAILURE;
256 plen=buf[2+ulen];
257 if(n < 2 + ulen + 1 + plen) return EC_GENERAL_FAILURE;
258 char user[256], pass[256];
259 memcpy(user, buf+2, ulen);
260 memcpy(pass, buf+2+ulen+1, plen);
261 if(!strcmp(user, auth_user) && !strcmp(pass, auth_pass)) return EC_SUCCESS;
262 return EC_NOT_ALLOWED;
265 static void* clientthread(void *data) {
266 struct thread *t = data;
267 t->state = SS_1_CONNECTED;
268 unsigned char buf[1024];
269 ssize_t n;
270 int ret;
271 int remotefd = -1;
272 enum authmethod am;
273 while((n = recv(t->client.fd, buf, sizeof buf, 0)) > 0) {
274 switch(t->state) {
275 case SS_1_CONNECTED:
276 am = check_auth_method(buf, n, &t->client);
277 if(am == AM_NO_AUTH) t->state = SS_3_AUTHED;
278 else if (am == AM_USERNAME) t->state = SS_2_NEED_AUTH;
279 send_auth_response(t->client.fd, am);
280 if(am == AM_INVALID) goto breakloop;
281 break;
282 case SS_2_NEED_AUTH:
283 ret = check_credentials(buf, n);
284 send_auth_response(t->client.fd, ret);
285 if(ret != EC_SUCCESS)
286 goto breakloop;
287 t->state = SS_3_AUTHED;
288 if(auth_ips) add_auth_ip(&t->client);
289 break;
290 case SS_3_AUTHED:
291 ret = connect_socks_target(buf, n, &t->client);
292 if(ret < 0) {
293 send_error(t->client.fd, ret*-1);
294 goto breakloop;
296 remotefd = ret;
297 send_error(t->client.fd, EC_SUCCESS);
298 copyloop(t->client.fd, remotefd);
299 goto breakloop;
303 breakloop:
305 if(remotefd != -1)
306 close(remotefd);
308 close(t->client.fd);
309 t->done = 1;
311 return 0;
314 static void collect(sblist *threads) {
315 size_t i;
316 for(i=0;i<sblist_getsize(threads);) {
317 struct thread* thread = *((struct thread**)sblist_get(threads, i));
318 if(thread->done) {
319 pthread_join(thread->pt, 0);
320 sblist_delete(threads, i);
321 free(thread);
322 } else
323 i++;
327 static int usage(void) {
328 dprintf(2,
329 "MicroSocks SOCKS5 Server\n"
330 "------------------------\n"
331 "usage: microsocks -1 -i listenip -p port -u user -P password\n"
332 "all arguments are optional.\n"
333 "by default listenip is 0.0.0.0 and port 1080.\n\n"
334 "option -1 activates auth_once mode: once a specific ip address\n"
335 "authed successfully with user/pass, it is added to a whitelist\n"
336 "and may use the proxy without auth.\n"
337 "this is handy for programs like firefox that don't support\n"
338 "user/pass auth. for it to work you'd basically make one connection\n"
339 "with another program that supports it, and then you can use firefox too.\n"
341 return 1;
344 /* prevent username and password from showing up in top. */
345 static void zero_arg(char *s) {
346 size_t i, l = strlen(s);
347 for(i=0;i<l;i++) s[i] = 0;
350 int main(int argc, char** argv) {
351 int c;
352 const char *listenip = "0.0.0.0";
353 unsigned port = 1080;
354 while((c = getopt(argc, argv, ":1i:p:u:P:")) != -1) {
355 switch(c) {
356 case '1':
357 auth_ips = sblist_new(sizeof(union sockaddr_union), 8);
358 break;
359 case 'u':
360 auth_user = strdup(optarg);
361 zero_arg(optarg);
362 break;
363 case 'P':
364 auth_pass = strdup(optarg);
365 zero_arg(optarg);
366 break;
367 case 'i':
368 listenip = optarg;
369 break;
370 case 'p':
371 port = atoi(optarg);
372 break;
373 case ':':
374 dprintf(2, "error: option -%c requires an operand\n", optopt);
375 case '?':
376 return usage();
379 if((auth_user && !auth_pass) || (!auth_user && auth_pass)) {
380 dprintf(2, "error: user and pass must be used together\n");
381 return 1;
383 if(auth_ips && !auth_pass) {
384 dprintf(2, "error: auth-once option must be used together with user/pass\n");
385 return 1;
387 struct server s;
388 sblist *threads = sblist_new(sizeof (struct thread*), 8);
389 if(server_setup(&s, listenip, port)) {
390 perror("server_setup");
391 return 1;
393 while(1) {
394 collect(threads);
395 struct client c;
396 struct thread *curr = malloc(sizeof (struct thread));
397 if(!curr) goto oom;
398 curr->done = 0;
399 if(server_waitclient(&s, &c)) continue;
400 curr->client = c;
401 if(!sblist_add(threads, &curr)) {
402 close(curr->client.fd);
403 free(curr);
404 oom:
405 dolog("rejecting connection due to OOM\n");
406 usleep(16); /* prevent 100% CPU usage in OOM situation */
407 continue;
409 pthread_attr_t *a = 0, attr;
410 if(pthread_attr_init(&attr) == 0) {
411 a = &attr;
412 pthread_attr_setstacksize(a, 8192); /* 4KB for us, 4KB for libc */
414 if(pthread_create(&curr->pt, a, clientthread, curr) != 0)
415 dolog("pthread_create failed. OOM?\n");
416 if(a) pthread_attr_destroy(&attr);