diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ab5ab3a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +*.o +*.out +out diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..cf2e46d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,78 @@ +name: Docker Image CI + +on: + push: + branches: [ master, 'claude/**' ] + tags: [ v* ] + pull_request: + +jobs: + compile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build microsocks + run: make + + - name: Build with SOMARK + run: make clean && make CFLAGS="-DSOMARK" + + - name: Test version flag + run: ./microsocks -V + + docker: + runs-on: ubuntu-latest + needs: compile + if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v') + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker meta + id: docker-meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository }} + tags: | + type=sha,prefix= + type=ref,event=tag + + - name: Build and push + uses: docker/build-push-action@v6 + with: + push: true + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8,linux/ppc64le,linux/riscv64,linux/s390x + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + outputs: type=local,dest=${{ github.workspace }}/out + + - name: Get metadata for artifact naming + id: meta + run: | + echo "commit-hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "repo-name=${GITHUB_REPOSITORY#*/}" >> $GITHUB_OUTPUT + + - name: Upload built binaries + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.meta.outputs.repo-name }}-${{ steps.meta.outputs.commit-hash }} + path: ${{ github.workspace }}/out diff --git a/.gitignore b/.gitignore index c9c6b85..99201ff 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.o *.out +out +microsocks diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..35cee39 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM alpine AS builder +RUN apk add --no-cache make gcc libc-dev +WORKDIR /app +ENV LDFLAGS=-static +COPY . . +RUN make + +FROM scratch +LABEL org.opencontainers.image.licenses=MIT +COPY --from=builder /app/microsocks / +ENTRYPOINT ["/microsocks"] diff --git a/Makefile b/Makefile index ecb3265..55e623a 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ prefix = /usr/local bindir = $(prefix)/bin PROG = microsocks -SRCS = sockssrv.c server.c sblist.c sblist_delete.c +SRCS = sockssrv.c server.c sblist.c sblist_delete.c bind2device.c OBJS = $(SRCS:.c=.o) LIBS = -lpthread diff --git a/README.md b/README.md index 2b7a04a..770e3d7 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ libc is not even 50 KB. that's easily usable even on the cheapest routers. command line options -------------------- - microsocks -1 -q -i listenip -p port -u user -P passw -b bindaddr -w wl + microsocks -1 -q -i listenip -p port -u user -P passw -b bindaddr -B bind2device -w wl all arguments are optional. by default listenip is 0.0.0.0 and port 1080. @@ -69,6 +69,29 @@ Supported SOCKS5 Features - IPv4, IPv6, DNS - TCP (no UDP at this time) +compile time options +-------------------- + + make CFLAGS=-DSOMARK + +microsocks can be compiled with SO_MARK support on linux 2.6.25+. This +enables 'marking' of outgoing packets for use with policy-based routing +which allows to route packets through a non-default interface. E.g.: + + ip rule add fwmark 1000 table 200 + ip route add default dev tun1 table 200 + microsocks -m 1000 + +will route all connections through device `tun1` + +Docker container +---------------- +You can run microsocks in a docker container: + + docker run --init -d -p 7777:1080 ghcr.io/rofl0r/microsocks + +Replace 7777 with the port microsocks will be accessible on. + Troubleshooting --------------- diff --git a/UPSTREAM_PR_ANALYSIS.md b/UPSTREAM_PR_ANALYSIS.md new file mode 100644 index 0000000..e501ad6 --- /dev/null +++ b/UPSTREAM_PR_ANALYSIS.md @@ -0,0 +1,87 @@ +# Upstream microsocks Open Pull Requests Analysis + +> Source: https://github.com/rofl0r/microsocks/pulls +> Date: 2026-02-01 +> Total open PRs: 11 + +## Features (8 PRs) + +### PR #98 — Add Dockerfile and GitHub Actions workflow +- **Author:** meanwhile131 | **Created:** 2026-01-06 +- **URL:** https://github.com/rofl0r/microsocks/pull/98 +- **Files:** `.dockerignore`, `.github/workflows/build.yml`, `.gitignore`, `Dockerfile`, `README.md` (+85 lines) +- **Summary:** Adds a multi-stage Dockerfile (Alpine builder to scratch runtime) and a CI/CD workflow that builds Docker images for 8 architectures (386, amd64, arm/v6, arm/v7, arm64/v8, ppc64le, riscv64, s390x). Images are published to GHCR and statically-built binaries are uploaded as artifacts. + +### PR #96 — Add SOCKS5 forwarding rules support +- **Author:** lwb1978 | **Created:** 2025-12-22 +- **URL:** https://github.com/rofl0r/microsocks/pull/96 +- **Files:** `sockssrv.c` (+305/-7 lines) +- **Summary:** Builds on PR #93's concept. Adds a `-f` flag for forwarding rules that selectively route matching destinations through upstream SOCKS5 proxies with optional authentication. Includes upstream handshake logic, socket timeouts (5s), and `-V` version flag. + +### PR #95 — Allow building with Windows + MinGW (Draft) +- **Author:** ccuser44 | **Created:** 2025-12-07 +- **URL:** https://github.com/rofl0r/microsocks/pull/95 +- **Files:** `dprintf.c` (new), `server.h`, `sockssrv.c`, `wsa2unix.h` (new) (+136/-7 lines) +- **Summary:** Adds Windows cross-compilation support via MinGW. Provides a `dprintf()` implementation, a `wsa2unix.h` compatibility header mapping Winsock error codes to Unix equivalents, conditional includes for `winsock2.h`/`ws2tcpip.h`, and replaces `poll()` with `WSAPoll()` on Windows. Author notes it is "mostly functional" with known `dprintf` caveats. + +### PR #93 — Forwarding rules +- **Author:** ohwgiles | **Created:** 2025-10-04 +- **URL:** https://github.com/rofl0r/microsocks/pull/93 +- **Files:** `sockssrv.c` (+126/-3 lines) +- **Summary:** Original implementation of selective forwarding rules using the syntax `match_name:match_port,[user:pass@]upstream_name:upstream_port,remote_name:remote_port`. Allows microsocks to act as a gateway to remote private networks via upstream SOCKS5 servers. PR #96 is a more complete evolution of this work. + +### PR #79 — Add `-B` bind device option +- **Author:** peppergrayxyz | **Created:** 2024-09-28 +- **URL:** https://github.com/rofl0r/microsocks/pull/79 +- **Files:** `Makefile`, `README.md`, `bind2device.c` (new), `bind2device.h` (new), `sockssrv.c` (+73/-4 lines) +- **Summary:** Adds a `-B` flag to bind outgoing sockets to a specific network interface. Uses `SO_BINDTODEVICE` on Linux and `IP_BOUND_IF`/`IPV6_BOUND_IF` on BSD/macOS. Includes a no-op stub for unsupported platforms. Follows up on issue #29. + +### PR #64 — Add `-t` option for idle exit timeout +- **Author:** chetan-reddy | **Created:** 2023-08-18 +- **URL:** https://github.com/rofl0r/microsocks/pull/64 +- **Files:** `sockssrv.c` (+41/-3 lines) +- **Summary:** Adds a `-t` flag specifying an idle exit timeout in seconds. When no connections arrive within the timeout period and no active threads exist, the server exits automatically. Uses non-blocking sockets via `fcntl()` and `poll()`. Useful for on-demand launches in resource-constrained environments. Tested on Linux and macOS. + +### PR #38 — Enable SO_MARK support on Linux via compile-time flag +- **Author:** grandrew | **Created:** 2021-06-14 +- **URL:** https://github.com/rofl0r/microsocks/pull/38 +- **Files:** `README.md`, `sockssrv.c` (+39 lines) +- **Summary:** Adds a `-m ` option to mark outgoing packets with Linux `SO_MARK` for policy-based routing. Enabled via a compile-time `SOMARK` flag. Includes README documentation with example commands showing how to route connections through specific network interfaces (e.g., `tun1`). + +### PR #29 — Allow binding to another (non-default) interface +- **Author:** tahajahangir | **Created:** 2020-09-25 +- **URL:** https://github.com/rofl0r/microsocks/pull/29 +- **Files:** `README.md`, `sockssrv.c` (+9/-2 lines) +- **Summary:** Adds a `-B` flag using `SO_BINDTODEVICE` to bind outgoing sockets to a specific network interface. A simpler, Linux-only predecessor to PR #79, which adds cross-platform support for the same feature. + +## Improvements (2 PRs) + +### PR #90 — Add timestamps to log output +- **Author:** ZsBT | **Created:** 2025-09-04 +- **URL:** https://github.com/rofl0r/microsocks/pull/90 +- **Files:** `sockssrv.c` (+17/-1 lines) +- **Summary:** Converts the `dolog` macro into a `static inline` function that prepends `[YYYY-MM-DD HH:MM:SS]` timestamps using `localtime_r()` and `vdprintf()`. Also adds a startup log message displaying the listening address and port. + +### PR #70 — Print timestamps in logs +- **Author:** Xenapte | **Created:** 2023-12-19 +- **URL:** https://github.com/rofl0r/microsocks/pull/70 +- **Files:** `sockssrv.c` (+6/-1 lines) +- **Summary:** Adds a `LOGTS()` macro that prepends `[MM-DD HH:MM:SS]` timestamps to log output using `strftime()` and `localtime_r()`. A lighter-weight predecessor to PR #90 which uses a different timestamp format (`YYYY-MM-DD`) and a different implementation approach. + +## Fixes (1 PR) + +### PR #86 — Fix minor nits in the manual page +- **Author:** ppentchev | **Created:** 2025-02-14 +- **URL:** https://github.com/rofl0r/microsocks/pull/86 +- **Files:** `microsocks.1` (+22/-13 lines) +- **Summary:** Fixes formatting issues in the man page: removes a stray `.Oc` bracket, improves grammar/punctuation in option descriptions (`-i`, `-w`), and applies the FreeBSD documentation convention of starting each sentence on a new line. + +## Notable Observations + +- **PR #93 and #96 overlap:** Both implement SOCKS5 forwarding rules. PR #96 by lwb1978 explicitly builds on PR #93 by ohwgiles and is more comprehensive (+305 lines vs +126 lines), adding authentication support and socket timeouts. +- **PR #29 and #79 overlap:** Both add `-B` bind-to-device support. PR #29 (2020) is Linux-only using `SO_BINDTODEVICE`; PR #79 (2024) is cross-platform, adding macOS support via `IP_BOUND_IF`/`IPV6_BOUND_IF`. +- **PR #70 and #90 overlap:** Both add timestamp prefixes to log output. PR #70 (2023) uses a macro with `[MM-DD HH:MM:SS]` format; PR #90 (2025) uses a function with `[YYYY-MM-DD HH:MM:SS]` format and also adds a startup message. +- **PR #95 (Windows/MinGW)** is a Draft PR, marked by its author as "mostly functional" with known caveats around `dprintf`. +- **PR #29 is the oldest** open PR (Sept 2020, over 5 years old). +- **PR #86 (man page fixes)** is the most straightforward and lowest-risk change. +- **Three pairs of related PRs** exist (#29/#79, #70/#90, #93/#96), where later PRs supersede or extend earlier ones. diff --git a/bind2device.c b/bind2device.c new file mode 100644 index 0000000..75d0244 --- /dev/null +++ b/bind2device.c @@ -0,0 +1,55 @@ +#undef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L + +#define _GNU_SOURCE +#define _DARWIN_C_SOURCE + +#include +#include +#include +#include +#include +#include + +#include "bind2device.h" + +#if (defined(IP_BOUND_IF) || defined(IPV6_BOUND_IF)) + +int bind2device(int sockfd, int socket_family, const char *device) +{ + int ifindex = if_nametoindex(device); + if (ifindex == 0) + return -1; + switch (socket_family) + { +#if defined(IPV6_BOUND_IF) + case AF_INET6: + return setsockopt(sockfd, IPPROTO_IPV6, IPV6_BOUND_IF, &ifindex, sizeof(ifindex)); +#endif +#if defined(IP_BOUND_IF) + case AF_INET: + return setsockopt(sockfd, IPPROTO_IP, IP_BOUND_IF, &ifindex, sizeof(ifindex)); +#endif + default: /* can't bind to interface for selected socket_family: operation not supported on socket */ + errno = EOPNOTSUPP; + return -1; + } +} + +#elif defined(SO_BINDTODEVICE) + +int bind2device(int sockfd, int socket_family, const char *device) +{ + return setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, device, strlen(device) + 1); +} + +#else +#pragma message "Platform does not support bind2device, generating stub." + +int bind2device(int sockfd, int socket_family, const char *device) +{ + errno = ENOSYS; /* unsupported platform: not implemented */ + return -1; +} + +#endif diff --git a/bind2device.h b/bind2device.h new file mode 100644 index 0000000..87a1bb8 --- /dev/null +++ b/bind2device.h @@ -0,0 +1,6 @@ +#ifndef BIND2DEVICE_H +#define BIND2DEVICE_H + +int bind2device(int sockfd, int socket_family, const char* device); + +#endif diff --git a/dprintf.c b/dprintf.c new file mode 100644 index 0000000..edf33da --- /dev/null +++ b/dprintf.c @@ -0,0 +1,36 @@ +#include +#include +#include + +int dprintf(int fd, const char *fmt, ...) +{ + char stack_buffer[256]; + char *buf = stack_buffer; + int len = sizeof(stack_buffer); + va_list ap; + int ret = -1; + + va_start(ap, fmt); + + /* try to just print it to stack if possible */ + ret = vsnprintf(buf, len, fmt, ap); + + if (ret >= len) { + /* dynamically allocate buffer */ + len = ret + 1; + buf = malloc(len); + if (buf) { + va_end(ap); + va_start(ap, fmt); + ret = vsnprintf(buf, len, fmt, ap); + } else { + ret = -1; + } + } + if (ret >= 0 && (write(fd, buf, ret) != ret)) { + ret = -1; + } + if (buf != stack_buffer) free(buf); + va_end(ap); + return ret; +} diff --git a/microsocks.1 b/microsocks.1 index 68bb189..fd4bd48 100644 --- a/microsocks.1 +++ b/microsocks.1 @@ -15,7 +15,6 @@ .Op Fl p Ar port .Op Fl u Ar user .Op Fl w Ar ips -.Oc .El .Ek .Sh DESCRIPTION @@ -24,8 +23,10 @@ is a multithreaded, tiny, portable SOCKS5 server with very moderate resource usage that you can run on your remote boxes to tunnel connections through them, if for some reason SSH doesn't cut it for you. It is very lightweight, and very light on resources too: for every client, a -thread with a low stack size is spawned. the main process basically doesn't -consume any resources at all. It is also designed to be robust: it handles +thread with a low stack size is spawned. +The main process basically doesn't +consume any resources at all. +It is also designed to be robust: it handles resource exhaustion gracefully by simply denying new connections, instead of calling .Xr abort 3 @@ -39,10 +40,13 @@ The following options are supported by .It Fl 1 Activates auth_once mode: once a specific IP address authorized successfully with user:password pair, it is added to a whitelist and may use the proxy -without authorization. This is handy for programs like Firefox that don't -support user:password authorization. For it to work you'd basically make one +without authorization. +This is handy for programs like Firefox that don't +support user:password authorization. +For it to work you'd basically make one connection with another program that supports it, and then you can use Firefox -too. This option requires options +too. +This option requires options .Fl u and .Fl P @@ -50,25 +54,30 @@ also to be specified. .It Fl b Ar ip Specifies IP address outgoing connections are bound to. .It Fl i Ar addr -Specifies local address to listen connections on. Host name or IP address can be -supplied. Default to +Specifies local address to listen for connections on. +Host name or IP address can be +supplied. +Default to .Cm 0.0.0.0 . .It Fl P -Specifies authorization password. This option requires +Specifies authorization password. +This option requires .Fl u also to be specified. .It Fl p -TCP port to listen to. Default to +TCP port to listen to. +Default to .Cm 1080 . .It Fl q Quiet mode: suppress logging messages. .It Fl u -Specifies authorization username value. This option requires +Specifies authorization username value. +This option requires .Fl P also to be specified. .It Fl w -A comma-separated whitelist of IP addresses, that may use the proxy without -authentication. e.g. +A comma-separated whitelist of IP addresses that may use the proxy without +authentication, e.g. .Cm -w 127.0.0.1,192.168.1.1.1,::1 or just .Cm -w 10.0.0.1 . diff --git a/server.h b/server.h index 5acf664..c1bf205 100644 --- a/server.h +++ b/server.h @@ -4,9 +4,21 @@ #undef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L -#include -#include -#include +#ifdef _WIN32 + #include + #include + + /* Add definition for pollfd to account for Windows not defining it */ + struct pollfd { + SOCKET fd; /* socket instead of int */ + short events; /* same values as POSIX (POLLIN, POLLOUT, etc.) */ + short revents; + }; +#else + #include + #include + #include +#endif #pragma RcB2 DEP "server.c" diff --git a/sockssrv.c b/sockssrv.c index d59fc27..1729023 100644 --- a/sockssrv.c +++ b/sockssrv.c @@ -22,19 +22,33 @@ */ #define _GNU_SOURCE +#ifdef _WIN32 + #include +#endif #include #define _POSIX_C_SOURCE 200809L #include #include #include +#include +#include #include #include -#include -#include +#ifdef _WIN32 + #include "wsa2unix.h" + #include "dprintf.c" +#else + #include + #include + #include +#endif #include #include +#include #include "server.h" #include "sblist.h" +#include "bind2device.h" +#define MICROSOCKS_VERSION "1.0.5-forward" /* timeout in microseconds on resource exhaustion to prevent excessive cpu usage. */ @@ -64,6 +78,10 @@ #define THREAD_STACK_SIZE 32*1024 #endif +#if defined(SOMARK) +static int somark; /* mark outgoing connections' packets for adv. routing */ +#endif + static int quiet; static const char* auth_user; static const char* auth_pass; @@ -71,6 +89,8 @@ static sblist* auth_ips; static pthread_rwlock_t auth_ips_lock = PTHREAD_RWLOCK_INITIALIZER; static const struct server* server; static union sockaddr_union bind_addr = {.v4.sin_family = AF_UNSPEC}; +static const char* bind_device; +static sblist *fwd_rules; enum socksstate { SS_1_CONNECTED, @@ -97,6 +117,17 @@ enum errorcode { EC_ADDRESSTYPE_NOT_SUPPORTED = 8, }; +struct fwd_rule { + char *match_name; + short match_port; + char *auth_buf; /* Username/Password request buffer (RFC-1929) */ + size_t auth_len; + char *upstream_name; + short upstream_port; + char *req_buf; /* Client Connection Request buffer to send to upstream */ + size_t req_len; +}; + struct thread { pthread_t pt; struct client client; @@ -111,11 +142,128 @@ struct thread { /* we log to stderr because it's not using line buffering, i.e. malloc which would need locking when called from different threads. for the same reason we use dprintf, which writes directly to an fd. */ -#define dolog(...) do { if(!quiet) dprintf(2, __VA_ARGS__); } while(0) +static inline void dolog(const char *fmt, ...) { + if(quiet) return; + char t[32] = {}; + struct tm tm_buf; + time_t secs = time(NULL); + + va_list args; + va_start(args, fmt); + + strftime(t, sizeof(t), "[%Y-%m-%d %T] ", localtime_r(&secs, &tm_buf)); + dprintf(fileno(stderr), "%s", t); + vdprintf(fileno(stderr), fmt, args); + + va_end(args); +} #else static void dolog(const char* fmt, ...) { } #endif +static int upstream_handshake(const struct fwd_rule* rule, unsigned char *client_buf, size_t client_buf_len, + int client_fd, int upstream_fd, unsigned short client_port) { + unsigned char sbuf[512]; + ssize_t r; + + if(rule->auth_buf) { + unsigned char handshake[4] = {5, 2, 0, 2}; + if (write(upstream_fd, handshake, 4) != 4) { + close(upstream_fd); + return -1; + } + } else { + unsigned char handshake[3] = {5, 1, 0}; + if (write(upstream_fd, handshake, 3) != 3) { + close(upstream_fd); + return -1; + } + } + + if (read(upstream_fd, sbuf, 2) != 2 || sbuf[0] != 5) { + close(upstream_fd); + return -1; + } + + if (sbuf[1] == 2) { + if (!rule->auth_buf) { + close(upstream_fd); + return -1; + } + if (write(upstream_fd, rule->auth_buf, rule->auth_len) != (ssize_t)rule->auth_len) { + close(upstream_fd); + return -1; + } + if (read(upstream_fd, sbuf, 2) != 2 || sbuf[0] != 1 || sbuf[1] != 0) { + close(upstream_fd); + return -1; + } + } else if (sbuf[1] != 0) { + close(upstream_fd); + return -1; + } + + if (write(upstream_fd, client_buf, client_buf_len) != (ssize_t)client_buf_len) { + close(upstream_fd); + return -1; + } + + size_t total = 0; + size_t need = 4; + + while (total < need) { + r = read(upstream_fd, sbuf + total, need - total); + if (r <= 0) { + close(upstream_fd); + return -1; + } + total += r; + } + + if (sbuf[1] != 0) { + close(upstream_fd); + return -sbuf[1]; + } + + size_t need_more = 0; + switch (sbuf[3]) { + case 1: + need_more = 4 + 2; + break; + case 4: + need_more = 16 + 2; + break; + case 3: + r = read(upstream_fd, sbuf + total, 1); + if (r != 1) { + close(upstream_fd); + return -1; + } + total += r; + need_more = sbuf[4] + 2; + break; + default: + close(upstream_fd); + return -EC_ADDRESSTYPE_NOT_SUPPORTED; + } + + while (total < need + need_more) { + r = read(upstream_fd, sbuf + total, (need + need_more) - total); + if (r <= 0) { + close(upstream_fd); + return -1; + } + total += r; + } + + if (write(client_fd, sbuf, total) != (ssize_t)total) { + close(upstream_fd); + return -1; + } + + return upstream_fd; +} + static struct addrinfo* addr_choose(struct addrinfo* list, union sockaddr_union* bindaddr) { int af = SOCKADDR_UNION_AF(bindaddr); if(af == AF_UNSPEC) return list; @@ -125,15 +273,19 @@ static struct addrinfo* addr_choose(struct addrinfo* list, union sockaddr_union* return list; } -static int connect_socks_target(unsigned char *buf, size_t n, struct client *client) { +static int connect_socks_target(unsigned char *buf, size_t n, struct client *client, int *used_rule) { + *used_rule = 0; + if(n < 5) return -EC_GENERAL_FAILURE; if(buf[0] != 5) return -EC_GENERAL_FAILURE; if(buf[1] != 1) return -EC_COMMAND_NOT_SUPPORTED; /* we support only CONNECT method */ if(buf[2] != 0) return -EC_GENERAL_FAILURE; /* malformed packet */ + size_t i; int af = AF_INET; size_t minlen = 4 + 4 + 2, l; char namebuf[256]; + struct fwd_rule *rule = NULL; struct addrinfo* remote; switch(buf[3]) { @@ -158,10 +310,33 @@ static int connect_socks_target(unsigned char *buf, size_t n, struct client *cli } unsigned short port; port = (buf[minlen-2] << 8) | buf[minlen-1]; + + char original_name[256]; + unsigned short original_port = port; + strncpy(original_name, namebuf, sizeof(original_name) - 1); + original_name[sizeof(original_name) - 1] = '\0'; + if(fwd_rules) { + for(i=0;imatch_name[0]=='\0' || strcmp(r->match_name, namebuf) == 0); + int port_match = (r->match_port == 0 || r->match_port == port); + if(name_match && port_match) { + rule = r; + *used_rule = 1; + strncpy(namebuf, r->upstream_name, sizeof(namebuf)-1); + namebuf[sizeof(namebuf)-1] = '\0'; + port = r->upstream_port; + break; + } + } + } + /* there's no suitable errorcode in rfc1928 for dns lookup failure */ if(resolve(namebuf, port, &remote)) return -EC_GENERAL_FAILURE; struct addrinfo* raddr = addr_choose(remote, &bind_addr); int fd = socket(raddr->ai_family, SOCK_STREAM, 0); + if(bind_device && bind2device(fd, raddr->ai_family, bind_device) == -1) + goto eval_errno; if(fd == -1) { eval_errno: if(fd != -1) close(fd); @@ -186,9 +361,18 @@ static int connect_socks_target(unsigned char *buf, size_t n, struct client *cli return -EC_GENERAL_FAILURE; } } + struct timeval tv = {5, 0}; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(tv)); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, (const char*)&tv, sizeof(tv)); + if(SOCKADDR_UNION_AF(&bind_addr) == raddr->ai_family && bindtoip(fd, &bind_addr) == -1) goto eval_errno; +#if defined(SOMARK) + if(somark != 0) { + setsockopt(fd, SOL_SOCKET, SO_MARK, &somark, sizeof(somark)); + } +#endif if(connect(fd, raddr->ai_addr, raddr->ai_addrlen) == -1) goto eval_errno; @@ -198,7 +382,20 @@ static int connect_socks_target(unsigned char *buf, size_t n, struct client *cli af = SOCKADDR_UNION_AF(&client->addr); void *ipdata = SOCKADDR_UNION_ADDRESS(&client->addr); inet_ntop(af, ipdata, clientname, sizeof clientname); - dolog("client[%d] %s: connected to %s:%d\n", client->fd, clientname, namebuf, port); + if (rule) { + dolog("client[%d] %s: %s:%d -> via %s:%d\n", client->fd, clientname, original_name, original_port, rule->upstream_name, rule->upstream_port); + } else { + dolog("client[%d] %s: connected to %s:%d\n", client->fd, clientname, namebuf, port); + } + } + + if (rule) { + int result = upstream_handshake(rule, buf, n, client->fd, fd, original_port); + if (result < 0) { + close(fd); + return result; + } + return result; } return fd; } @@ -276,7 +473,11 @@ static void copyloop(int fd1, int fd2) { /* inactive connections are reaped after 15 min to free resources. usually programs send keep-alive packets so this should only happen when a connection is really unused. */ +#ifdef _WIN32 + switch(WSAPoll(fds, 2, 60*15*1000)) { +#else switch(poll(fds, 2, 60*15*1000)) { +#endif case 0: return; case -1: @@ -322,6 +523,7 @@ static int handshake(struct thread *t) { ssize_t n; int ret; enum authmethod am; + int used_rule = 0; t->state = SS_1_CONNECTED; while((n = recv(t->client.fd, buf, sizeof buf, 0)) > 0) { switch(t->state) { @@ -345,12 +547,14 @@ static int handshake(struct thread *t) { } break; case SS_3_AUTHED: - ret = connect_socks_target(buf, n, &t->client); + ret = connect_socks_target(buf, n, &t->client, &used_rule); if(ret < 0) { send_error(t->client.fd, ret*-1); return -1; } - send_error(t->client.fd, EC_SUCCESS); + if (!used_rule) { + send_error(t->client.fd, EC_SUCCESS); + } return ret; } } @@ -382,25 +586,158 @@ static void collect(sblist *threads) { } } +static short host_get_port(char *name) { + int p,n; + char *c; + if((c = strrchr(name, ':')) && sscanf(c+1,"%d%n",&p, &n)==1 && n == (int)(strlen(c + 1)) && p >= 0 && p < USHRT_MAX) + return (*c='\0'),(short)p; + else + return -1; +} + +static int fwd_rules_add(char *str) { + char *match = NULL, *upstream = NULL, *remote = NULL; + unsigned short match_port, upstream_port, remote_port; + int ncred; + + if(sscanf(str, "%m[^,],%n%m[^,],%ms\n", &match, &ncred, &upstream, &remote) != 3) + return 1; + + match_port = host_get_port(match); + upstream_port = host_get_port(upstream); + remote_port = host_get_port(remote); + + if(match_port < 0 || upstream_port <= 0 || remote_port < 0) { + free(match); + free(upstream); + free(remote); + return 1; + } + + char *match_copy = strdup(match); + char *upstream_copy = strdup(upstream); + char *remote_copy = strdup(remote); + + struct fwd_rule *rule = (struct fwd_rule*)malloc(sizeof(struct fwd_rule)); + if (!rule) { + free(match_copy); + free(upstream_copy); + free(remote_copy); + free(match); + free(upstream); + free(remote); + return 1; + } + + if(strcmp(match_copy, "0.0.0.0") == 0 || strcmp(match_copy, "*") == 0) { + free(match_copy); + rule->match_name = strdup(""); + } else { + rule->match_name = match_copy; + } + rule->match_port = match_port; + rule->auth_buf = NULL; + rule->auth_len = 0; + + char *at_sign = strchr(upstream_copy, '@'); + if (at_sign) { + *at_sign = '\0'; + char *auth_part = upstream_copy; + char *host_part = at_sign + 1; + char *colon = strchr(auth_part, ':'); + if (!colon) { + free(rule); + free(upstream_copy); + free(remote_copy); + free(match); + free(upstream); + free(remote); + return 1; + } + *colon++ = '\0'; + char *username = auth_part; + char *password = colon; + size_t ulen = strlen(username); + size_t plen = strlen(password); + if (ulen > 255 || plen > 255) { + free(rule); + free(upstream_copy); + free(remote_copy); + free(match); + free(upstream); + free(remote); + return 1; + } + rule->auth_len = 1 + 1 + ulen + 1 + plen; + rule->auth_buf = malloc(rule->auth_len); + rule->auth_buf[0] = 1; + rule->auth_buf[1] = ulen; + memcpy(&rule->auth_buf[2], username, ulen); + rule->auth_buf[2 + ulen] = plen; + memcpy(&rule->auth_buf[3 + ulen], password, plen); + rule->upstream_name = strdup(host_part); + rule->upstream_port = upstream_port; + /* hide from ps */ + memset(str+ncred, '*', ulen+1+plen); + } else { + rule->upstream_name = strdup(upstream_copy); + rule->upstream_port = upstream_port; + } + + free(upstream_copy); + short rlen = strlen(remote_copy); + rule->req_len = 3 + 1 + 1 + rlen + 2; + rule->req_buf = (char*)malloc(rule->req_len); + rule->req_buf[0] = 5; + rule->req_buf[1] = 1; + rule->req_buf[2] = 0; + rule->req_buf[3] = 3; + rule->req_buf[4] = rlen; + memcpy(&rule->req_buf[5], remote_copy, rlen); + unsigned short rport = remote_port ? remote_port : 0; + rule->req_buf[5 + rlen] = (rport >> 8) & 0xFF; + rule->req_buf[5 + rlen + 1] = (rport & 0xFF); + free(remote_copy); + sblist_add(fwd_rules, rule); + free(match); + free(upstream); + free(remote); + + return 0; +} + static int usage(void) { dprintf(2, "MicroSocks SOCKS5 Server\n" "------------------------\n" - "usage: microsocks -1 -q -i listenip -p port -u user -P pass -b bindaddr -w ips\n" + "usage: microsocks -1 -q -i listenip -p port -u user -P pass -b bindaddr -B bind2device -w ips -t timeout -f fwdrule\n" +#if defined(SOMARK) + " -m mark\n" +#endif "all arguments are optional.\n" "by default listenip is 0.0.0.0 and port 1080.\n\n" "option -q disables logging.\n" + "option -t specifies an idle exit timeout in seconds. default is to wait forever\n" "option -b specifies which ip outgoing connections are bound to\n" "option -w allows to specify a comma-separated whitelist of ip addresses,\n" " that may use the proxy without user/pass authentication.\n" " e.g. -w 127.0.0.1,192.168.1.1.1,::1 or just -w 10.0.0.1\n" " to allow access ONLY to those ips, choose an impossible to guess user/pw combo.\n" "option -1 activates auth_once mode: once a specific ip address\n" +#if defined(SOMARK) + "option -m marks outgoing connections' packets with specified SO_MARK id\n" +#endif " authed successfully with user/pass, it is added to a whitelist\n" " and may use the proxy without auth.\n" " this is handy for programs like firefox that don't support\n" " user/pass auth. for it to work you'd basically make one connection\n" " with another program that supports it, and then you can use firefox too.\n" + "option -f specifies a forwarding rule of the form\n" + " match_name:match_port,[user:password@]upstream_name:upstream_port,remote_name:remote_port\n" + " this will cause requests that /match/ to be renamed to /remote/\n" + " and sent to the /upstream/ SOCKS5 proxy server.\n" + " this option may be specified multiple times.\n" + "option -V prints version information and exits.\n" ); return 1; } @@ -416,7 +753,12 @@ int main(int argc, char** argv) { const char *listenip = "0.0.0.0"; char *p, *q; unsigned port = 1080; - while((ch = getopt(argc, argv, ":1qb:i:p:u:P:w:")) != -1) { + unsigned idle_timeout = 0; +#if defined(SOMARK) + while((ch = getopt(argc, argv, ":1qb:B:i:m:p:t:u:P:w:f:V")) != -1) { +#else + while((ch = getopt(argc, argv, ":1qb:B:i:p:t:u:P:w:f:V")) != -1) { +#endif switch(ch) { case 'w': /* fall-through */ case '1': @@ -442,6 +784,10 @@ int main(int argc, char** argv) { case 'b': resolve_sa(optarg, 0, &bind_addr); break; + case 'B': + bind_device = strdup(optarg); + zero_arg(optarg); + break; case 'u': auth_user = strdup(optarg); zero_arg(optarg); @@ -456,11 +802,28 @@ int main(int argc, char** argv) { case 'p': port = atoi(optarg); break; + case 't': + idle_timeout = atoi(optarg); + break; + case 'f': + if(!fwd_rules) + fwd_rules = sblist_new(sizeof(struct fwd_rule), 16); + if(fwd_rules_add(optarg)) + return dprintf(2, "error: could not parse forwarding rule %s\n", optarg), 1; + break; +#if defined(SOMARK) + case 'm': + somark = atoi(optarg); + break; +#endif case ':': dprintf(2, "error: option -%c requires an operand\n", optopt); /* fall through */ case '?': return usage(); + case 'V': + dprintf(1, "MicroSocks %s\n", MICROSOCKS_VERSION); + return 0; } } if((auth_user && !auth_pass) || (!auth_user && auth_pass)) { @@ -471,7 +834,11 @@ int main(int argc, char** argv) { dprintf(2, "error: -1/-w options must be used together with user/pass\n"); return 1; } +#ifdef _WIN32 + /* Windows doesn't kill processes on pipe failure */ +#else signal(SIGPIPE, SIG_IGN); +#endif struct server s; sblist *threads = sblist_new(sizeof (struct thread*), 8); if(server_setup(&s, listenip, port)) { @@ -479,9 +846,36 @@ int main(int argc, char** argv) { return 1; } server = &s; + dolog("Listening on %s:%d\n", listenip, port); + + if (idle_timeout && fcntl(s.fd, F_SETFL, fcntl(s.fd, F_GETFL, 0) | O_NONBLOCK)) { + perror("fcntl O_NONBLOCK"); + return 1; + } while(1) { - collect(threads); + while(1) { + collect(threads); + if (!idle_timeout) break; + struct pollfd fds[1] = { + [0] = {.fd = s.fd, .events = POLLIN}, + }; + switch(poll(fds, 1, idle_timeout*1000)) { + case 0: + if (sblist_getsize(threads) == 0) { + dprintf(2, "idle timeout exit\n"); + return 0; + } + continue; + case -1: + if(errno != EINTR && errno != EAGAIN) { + perror("poll"); + return 1; + } + continue; + } + break; + } struct client c; struct thread *curr = malloc(sizeof (struct thread)); if(!curr) goto oom; @@ -492,6 +886,12 @@ int main(int argc, char** argv) { usleep(FAILURE_TIMEOUT); continue; } + if (idle_timeout && fcntl(c.fd, F_SETFL, fcntl(c.fd, F_GETFL, 0) & ~O_NONBLOCK)) { + perror("fcntl ~O_NONBLOCK"); + close(c.fd); + free(curr); + continue; + } curr->client = c; if(!sblist_add(threads, &curr)) { close(curr->client.fd); diff --git a/wsa2unix.h b/wsa2unix.h new file mode 100644 index 0000000..ded79e0 --- /dev/null +++ b/wsa2unix.h @@ -0,0 +1,64 @@ +/* Define POLLIN values manually if not defined by some compiler */ +#ifndef POLLIN + #define POLLIN 0x001 + #define POLLPRI 0x002 + #define POLLOUT 0x004 + #define POLLERR 0x008 + #define POLLHUP 0x010 + #define POLLNVAL 0x020 + #define POLLRDNORM 0x100 + #define POLLRDBAND 0x200 + #define POLLWRNORM 0x010 + #define POLLWRBAND 0x400 +#endif + +/* Operation would block */ +#define EWOULDBLOCK WSAEWOULDBLOCK +#define EAGAIN WSAEWOULDBLOCK + +/* Operation now in progress */ +#define EINPROGRESS WSAEINPROGRESS +#define EALREADY WSAEALREADY + +/* Socket errors */ +#define ENOTSOCK WSAENOTSOCK +#define EDESTADDRREQ WSAEDESTADDRREQ +#define EMSGSIZE WSAEMSGSIZE +#define EPROTOTYPE WSAEPROTOTYPE +#define ENOPROTOOPT WSAENOPROTOOPT +#define EPROTONOSUPPORT WSAEPROTONOSUPPORT +#define ESOCKTNOSUPPORT WSAESOCKTNOSUPPORT +#define EOPNOTSUPP WSAEOPNOTSUPP +#define EPFNOSUPPORT WSAEPFNOSUPPORT +#define EAFNOSUPPORT WSAEAFNOSUPPORT + +/* Address errors */ +#define EADDRINUSE WSAEADDRINUSE +#define EADDRNOTAVAIL WSAEADDRNOTAVAIL + +/* Network subsystem errors */ +#define ENETDOWN WSAENETDOWN +#define ENETUNREACH WSAENETUNREACH +#define ENETRESET WSAENETRESET + +/* Connection errors */ +#define ECONNABORTED WSAECONNABORTED +#define ECONNRESET WSAECONNRESET +#define ENOBUFS WSAENOBUFS +#define EISCONN WSAEISCONN +#define ENOTCONN WSAENOTCONN +#define ESHUTDOWN WSAESHUTDOWN +#define ETIMEDOUT WSAETIMEDOUT +#define ECONNREFUSED WSAECONNREFUSED + +/* Host errors */ +#define EHOSTUNREACH WSAEHOSTUNREACH + +/* Some misc stuff */ +#define EINTR WSAEINTR +#define EFAULT WSAEFAULT +#define EINVAL WSAEINVAL +#define EMFILE WSAEMFILE +#define EACCES WSAEACCES +#define EPERM WSAEACCES +#define ENOMEM WSA_NOT_ENOUGH_MEMORY