diff --git a/deployments/openvpn/Dockerfile b/deployments/openvpn/Dockerfile index e48987b..b852efb 100644 --- a/deployments/openvpn/Dockerfile +++ b/deployments/openvpn/Dockerfile @@ -1,18 +1,25 @@ -# 戎行专用:OpenVPN 客户端 + Dante SOCKS5(仅内网代理) +# 戎行专用:OpenVPN 客户端 + microsocks SOCKS5(走系统路由/tun0) +FROM alpine:3.19 AS socks-builder + +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ + && apk add --no-cache build-base + +WORKDIR /src +COPY third_party/microsocks/ ./ +RUN make && strip microsocks + FROM alpine:3.19 -# 与业务镜像一致,使用国内 apk 源 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ && apk add --no-cache \ openvpn \ bash \ iproute2 \ - dante-server \ curl +COPY --from=socks-builder /src/microsocks /usr/local/bin/microsocks COPY entrypoint.sh /entrypoint.sh -COPY sockd.conf.template /etc/sockd.conf.template -RUN chmod +x /entrypoint.sh +RUN chmod +x /entrypoint.sh /usr/local/bin/microsocks ENV VPN_DIR=/vpn \ SOCKS_HOST=0.0.0.0 \ diff --git a/deployments/openvpn/README.md b/deployments/openvpn/README.md index 6a88074..7b432b1 100644 --- a/deployments/openvpn/README.md +++ b/deployments/openvpn/README.md @@ -4,9 +4,9 @@ | 文件 | 用途 | |------|------| -| `Dockerfile` | OpenVPN + dante-server(sockd)自建镜像 | +| `Dockerfile` | OpenVPN + 自编译 microsocks | | `entrypoint.sh` | 启动 VPN(仅路由戎行 IP)并监听 SOCKS5 `:1080` | -| `sockd.conf.template` | Dante SOCKS 配置模板 | +| `third_party/microsocks/` | SOCKS5 源码(避免 Alpine 无包 / Dante 绑错网卡) | | `tiany0721.ovpn.example` | ovpn 模板 | | `docker-compose.rongxing-vpn.snippet.yml` | 独立编排参考(prod 已内置) | | `upload-checklist.txt` | 上传清单 | diff --git a/deployments/openvpn/entrypoint.sh b/deployments/openvpn/entrypoint.sh index fcacefe..ee34455 100644 --- a/deployments/openvpn/entrypoint.sh +++ b/deployments/openvpn/entrypoint.sh @@ -1,5 +1,5 @@ #!/bin/bash -# OpenVPN 连通后拉起 SOCKS5(dante/sockd);仅服务戎行内网代理。 +# OpenVPN 连通后拉起 microsocks;出口跟随系统路由(含 tun0),避免 Dante 绑错网卡。 set -euo pipefail VPN_DIR="${VPN_DIR:-/vpn}" @@ -48,15 +48,25 @@ for i in $(seq 1 90); do sleep 1 done -# 生成 dante 配置并后台启动 sockd -sed "s/__SOCKS_PORT__/${SOCKS_PORT}/g" /etc/sockd.conf.template > /etc/sockd.conf -sockd -f /etc/sockd.conf -D -SOCKS_PID="$(pidof sockd | awk '{print $1}')" -if [ -z "${SOCKS_PID}" ]; then - echo "[rongxing-vpn] sockd 启动失败" +echo "[rongxing-vpn] 路由表:" +ip route || true + +# 直连自检(不阻断) +if curl -sS -m 5 -o /tmp/rx_probe.out -w "[rongxing-vpn] 直连探测 HTTP %{http_code}\n" "http://${RONGXING_HOST}:7007/" ; then + head -c 200 /tmp/rx_probe.out 2>/dev/null || true + echo +else + echo "[rongxing-vpn] 直连探测失败: http://${RONGXING_HOST}:7007/" +fi + +microsocks -i "$SOCKS_HOST" -p "$SOCKS_PORT" & +SOCKS_PID=$! +sleep 1 +if ! kill -0 "$SOCKS_PID" 2>/dev/null; then + echo "[rongxing-vpn] microsocks 启动失败" exit 1 fi -echo "[rongxing-vpn] SOCKS5 已监听 :${SOCKS_PORT} (pid=${SOCKS_PID})" +echo "[rongxing-vpn] SOCKS5 已监听 :${SOCKS_PORT} (microsocks pid=${SOCKS_PID})" cleanup() { kill "$SOCKS_PID" 2>/dev/null || true diff --git a/deployments/openvpn/sockd.conf.template b/deployments/openvpn/sockd.conf.template deleted file mode 100644 index 3539b3b..0000000 --- a/deployments/openvpn/sockd.conf.template +++ /dev/null @@ -1,14 +0,0 @@ -# Dante SOCKS5 — entrypoint 会替换 __SOCKS_PORT__ -logoutput: stderr -internal: 0.0.0.0 port = __SOCKS_PORT__ -external: eth0 -socksmethod: none -clientmethod: none - -client pass { - from: 0.0.0.0/0 to: 0.0.0.0/0 -} - -socks pass { - from: 0.0.0.0/0 to: 0.0.0.0/0 -} diff --git a/deployments/openvpn/third_party/microsocks/.gitignore b/deployments/openvpn/third_party/microsocks/.gitignore new file mode 100644 index 0000000..c9c6b85 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/.gitignore @@ -0,0 +1,3 @@ +*.o +*.out + diff --git a/deployments/openvpn/third_party/microsocks/COPYING b/deployments/openvpn/third_party/microsocks/COPYING new file mode 100644 index 0000000..bfb6c9c --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/COPYING @@ -0,0 +1,24 @@ +microSocks is licensed under the following standard MIT license: + +---------------------------------------------------------------------- +Copyright © 2017 rofl0r. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------- diff --git a/deployments/openvpn/third_party/microsocks/Makefile b/deployments/openvpn/third_party/microsocks/Makefile new file mode 100644 index 0000000..ecb3265 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/Makefile @@ -0,0 +1,35 @@ +# if you want to change/override some variables, do so in a file called +# config.mak, which is gets included automatically if it exists. + +prefix = /usr/local +bindir = $(prefix)/bin + +PROG = microsocks +SRCS = sockssrv.c server.c sblist.c sblist_delete.c +OBJS = $(SRCS:.c=.o) + +LIBS = -lpthread + +CFLAGS += -Wall -std=c99 + +INSTALL = ./install.sh + +-include config.mak + +all: $(PROG) + +install: $(PROG) + $(INSTALL) -D -m 755 $(PROG) $(DESTDIR)$(bindir)/$(PROG) + +clean: + rm -f $(PROG) + rm -f $(OBJS) + +%.o: %.c + $(CC) $(CPPFLAGS) $(CFLAGS) $(INC) $(PIC) -c -o $@ $< + +$(PROG): $(OBJS) + $(CC) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ + +.PHONY: all clean install + diff --git a/deployments/openvpn/third_party/microsocks/README.md b/deployments/openvpn/third_party/microsocks/README.md new file mode 100644 index 0000000..cc5b9a3 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/README.md @@ -0,0 +1,65 @@ +MicroSocks - multithreaded, small, efficient SOCKS5 server. +=========================================================== + +a SOCKS5 service 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's very lightweight, and very light on resources too: + +for every client, a thread with a stack size of 8KB is spawned. +the main process basically doesn't consume any resources at all. + +the only limits are the amount of file descriptors and the RAM. + +It's also designed to be robust: it handles resource exhaustion +gracefully by simply denying new connections, instead of calling abort() +as most other programs do these days. + +another plus is ease-of-use: no config file necessary, everything can be +done from the command line and doesn't even need any parameters for quick +setup. + +History +------- + +This is the successor of "rocksocks5", and it was written with +different goals in mind: + +- prefer usage of standard libc functions over homegrown ones +- no artificial limits +- do not aim for minimal binary size, but for minimal source code size, + and maximal readability, reusability, and extensibility. + +as a result of that, ipv4, dns, and ipv6 is supported out of the box +and can use the same code, while rocksocks5 has several compile time +defines to bring down the size of the resulting binary to extreme values +like 10 KB static linked when only ipv4 support is enabled. + +still, if optimized for size, *this* program when static linked against musl +libc is not even 50 KB. that's easily usable even on the cheapest routers. + +command line options +-------------------- + + microsocks -1 -i listenip -p port -u user -P password -b bindaddr + +all arguments are optional. +by default listenip is 0.0.0.0 and port 1080. + +option -1 activates auth_once mode: once a specific ip address +authed successfully with user/pass, it is added to a whitelist +and may use the proxy without auth. +this is handy for programs like firefox that don't support +user/pass auth. for it to work you'd basically make one connection +with another program that supports it, and then you can use firefox too. +for example, authenticate once using curl: + + curl --socks5 user:password@listenip:port anyurl + + +Supported SOCKS5 Features +------------------------- +- authentication: none, password, one-time +- IPv4, IPv6, DNS +- TCP (no UDP at this time) + diff --git a/deployments/openvpn/third_party/microsocks/create-dist.sh b/deployments/openvpn/third_party/microsocks/create-dist.sh new file mode 100644 index 0000000..279774d --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/create-dist.sh @@ -0,0 +1,28 @@ +#!/bin/sh +if [ -z "$VER" ] ; then + echo set VER! + exit +fi +me=`pwd` + +proj=microsocks +projver=${proj}-${VER} + +tempdir=/tmp/${proj}-0000 +rm -rf "$tempdir" +mkdir -p "$tempdir" + +cd $tempdir +GITDIR=https://github.com/rofl0r/$proj +GITDIR=$me +git clone "$GITDIR" $projver + +rm -rf $projver/.git +rm -rf $projver/docs +rm -f $projver/.gitignore +rm -f $projver/create-dist.sh + +tar cf $proj.tar $projver/ +xz -z -9 -e $proj.tar +mv $proj.tar.xz $me/$projver.tar.xz +rm -rf "$tempdir" diff --git a/deployments/openvpn/third_party/microsocks/install.sh b/deployments/openvpn/third_party/microsocks/install.sh new file mode 100644 index 0000000..0410883 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/install.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# +# Written by Rich Felker, originally as part of musl libc. +# Multi-licensed under MIT, 0BSD, and CC0. +# +# This is an actually-safe install command which installs the new +# file atomically in the new location, rather than overwriting +# existing files. +# + +usage() { +printf "usage: %s [-D] [-l] [-m mode] src dest\n" "$0" 1>&2 +exit 1 +} + +mkdirp= +symlink= +mode=755 + +while getopts Dlm: name ; do +case "$name" in +D) mkdirp=yes ;; +l) symlink=yes ;; +m) mode=$OPTARG ;; +?) usage ;; +esac +done +shift $(($OPTIND - 1)) + +test "$#" -eq 2 || usage +src=$1 +dst=$2 +tmp="$dst.tmp.$$" + +case "$dst" in +*/) printf "%s: %s ends in /\n", "$0" "$dst" 1>&2 ; exit 1 ;; +esac + +set -C +set -e + +if test "$mkdirp" ; then +umask 022 +case "$2" in +*/*) mkdir -p "${dst%/*}" ;; +esac +fi + +trap 'rm -f "$tmp"' EXIT INT QUIT TERM HUP + +umask 077 + +if test "$symlink" ; then +ln -s "$1" "$tmp" +else +cat < "$1" > "$tmp" +chmod "$mode" "$tmp" +fi + +mv -f "$tmp" "$2" +test -d "$2" && { +rm -f "$2/$tmp" +printf "%s: %s is a directory\n" "$0" "$dst" 1>&2 +exit 1 +} + +exit 0 diff --git a/deployments/openvpn/third_party/microsocks/sblist.c b/deployments/openvpn/third_party/microsocks/sblist.c new file mode 100644 index 0000000..96bbebc --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/sblist.c @@ -0,0 +1,73 @@ +#undef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#include "sblist.h" +#include +#include +#include +#define MY_PAGE_SIZE 4096 + +sblist* sblist_new(size_t itemsize, size_t blockitems) { + sblist* ret = (sblist*) malloc(sizeof(sblist)); + sblist_init(ret, itemsize, blockitems); + return ret; +} + +static void sblist_clear(sblist* l) { + l->items = NULL; + l->capa = 0; + l->count = 0; +} + +void sblist_init(sblist* l, size_t itemsize, size_t blockitems) { + if(l) { + l->blockitems = blockitems ? blockitems : MY_PAGE_SIZE / itemsize; + l->itemsize = itemsize; + sblist_clear(l); + } +} + +void sblist_free_items(sblist* l) { + if(l) { + if(l->items) free(l->items); + sblist_clear(l); + } +} + +void sblist_free(sblist* l) { + if(l) { + sblist_free_items(l); + free(l); + } +} + +char* sblist_item_from_index(sblist* l, size_t idx) { + return l->items + (idx * l->itemsize); +} + +void* sblist_get(sblist* l, size_t item) { + if(item < l->count) return (void*) sblist_item_from_index(l, item); + return NULL; +} + +int sblist_set(sblist* l, void* item, size_t pos) { + if(pos >= l->count) return 0; + memcpy(sblist_item_from_index(l, pos), item, l->itemsize); + return 1; +} + +int sblist_grow_if_needed(sblist* l) { + char* temp; + if(l->count == l->capa) { + temp = realloc(l->items, (l->capa + l->blockitems) * l->itemsize); + if(!temp) return 0; + l->capa += l->blockitems; + l->items = temp; + } + return 1; +} + +int sblist_add(sblist* l, void* item) { + if(!sblist_grow_if_needed(l)) return 0; + l->count++; + return sblist_set(l, item, l->count - 1); +} diff --git a/deployments/openvpn/third_party/microsocks/sblist.h b/deployments/openvpn/third_party/microsocks/sblist.h new file mode 100644 index 0000000..f0d1846 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/sblist.h @@ -0,0 +1,92 @@ +#ifndef SBLIST_H +#define SBLIST_H + +/* this file is part of libulz, as of commit 8ab361a27743aaf025323ee43b8b8876dc054fdd + modified for direct inclusion in microsocks. */ + + +#ifdef __cplusplus +extern "C" { +#endif + +#include +/* + * simple buffer list. + * + * this thing here is basically a generic dynamic array + * will realloc after every blockitems inserts + * can store items of any size. + * + * so think of it as a by-value list, as opposed to a typical by-ref list. + * you typically use it by having some struct on the stack, and pass a pointer + * to sblist_add, which will copy the contents into its internal memory. + * + */ + +typedef struct { + size_t itemsize; + size_t blockitems; + size_t count; + size_t capa; + char* items; +} sblist; + +#define sblist_getsize(X) ((X)->count) +#define sblist_get_count(X) ((X)->count) +#define sblist_empty(X) ((X)->count == 0) + +// for dynamic style +sblist* sblist_new(size_t itemsize, size_t blockitems); +void sblist_free(sblist* l); + +//for static style +void sblist_init(sblist* l, size_t itemsize, size_t blockitems); +void sblist_free_items(sblist* l); + +// accessors +void* sblist_get(sblist* l, size_t item); +// returns 1 on success, 0 on OOM +int sblist_add(sblist* l, void* item); +int sblist_set(sblist* l, void* item, size_t pos); +void sblist_delete(sblist* l, size_t item); +char* sblist_item_from_index(sblist* l, size_t idx); +int sblist_grow_if_needed(sblist* l); +int sblist_insert(sblist* l, void* item, size_t pos); +/* same as sblist_add, but returns list index of new item, or -1 */ +size_t sblist_addi(sblist* l, void* item); +void sblist_sort(sblist *l, int (*compar)(const void *, const void *)); +/* insert element into presorted list, returns listindex of new entry or -1*/ +size_t sblist_insert_sorted(sblist* l, void* o, int (*compar)(const void *, const void *)); + +#ifndef __COUNTER__ +#define __COUNTER__ __LINE__ +#endif + +#define __sblist_concat_impl( x, y ) x##y +#define __sblist_macro_concat( x, y ) __sblist_concat_impl( x, y ) +#define __sblist_iterator_name __sblist_macro_concat(sblist_iterator, __COUNTER__) + +// use with custom iterator variable +#define sblist_iter_counter(LIST, ITER, PTR) \ + for(size_t ITER = 0; (PTR = sblist_get(LIST, ITER)), ITER < sblist_getsize(LIST); ITER++) + +// use with custom iterator variable, which is predeclared +#define sblist_iter_counter2(LIST, ITER, PTR) \ + for(ITER = 0; (PTR = sblist_get(LIST, ITER)), ITER < sblist_getsize(LIST); ITER++) + +// use with custom iterator variable, which is predeclared and signed +// useful for a loop which can delete items from the list, and then decrease the iterator var. +#define sblist_iter_counter2s(LIST, ITER, PTR) \ + for(ITER = 0; (PTR = sblist_get(LIST, ITER)), ITER < (ssize_t) sblist_getsize(LIST); ITER++) + + +// uses "magic" iterator variable +#define sblist_iter(LIST, PTR) sblist_iter_counter(LIST, __sblist_iterator_name, PTR) + +#ifdef __cplusplus +} +#endif + +#pragma RcB2 DEP "sblist.c" "sblist_delete.c" + +#endif diff --git a/deployments/openvpn/third_party/microsocks/sblist_delete.c b/deployments/openvpn/third_party/microsocks/sblist_delete.c new file mode 100644 index 0000000..a18209a --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/sblist_delete.c @@ -0,0 +1,9 @@ +#include "sblist.h" +#include + +void sblist_delete(sblist* l, size_t item) { + if (l->count && item < l->count) { + memmove(sblist_item_from_index(l, item), sblist_item_from_index(l, item + 1), (sblist_getsize(l) - (item + 1)) * l->itemsize); + l->count--; + } +} diff --git a/deployments/openvpn/third_party/microsocks/server.c b/deployments/openvpn/third_party/microsocks/server.c new file mode 100644 index 0000000..9f0ab9a --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/server.c @@ -0,0 +1,64 @@ +#include "server.h" +#include +#include +#include + +int resolve(const char *host, unsigned short port, struct addrinfo** addr) { + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_flags = AI_PASSIVE, + }; + char port_buf[8]; + snprintf(port_buf, sizeof port_buf, "%u", port); + return getaddrinfo(host, port_buf, &hints, addr); +} + +int resolve_sa(const char *host, unsigned short port, union sockaddr_union *res) { + struct addrinfo *ainfo = 0; + int ret; + SOCKADDR_UNION_AF(res) = AF_UNSPEC; + if((ret = resolve(host, port, &ainfo))) return ret; + memcpy(res, ainfo->ai_addr, ainfo->ai_addrlen); + freeaddrinfo(ainfo); + return 0; +} + +int bindtoip(int fd, union sockaddr_union *bindaddr) { + socklen_t sz = SOCKADDR_UNION_LENGTH(bindaddr); + if(sz) + return bind(fd, (struct sockaddr*) bindaddr, sz); + return 0; +} + +int server_waitclient(struct server *server, struct client* client) { + socklen_t clen = sizeof client->addr; + return ((client->fd = accept(server->fd, (void*)&client->addr, &clen)) == -1)*-1; +} + +int server_setup(struct server *server, const char* listenip, unsigned short port) { + struct addrinfo *ainfo = 0; + if(resolve(listenip, port, &ainfo)) return -1; + struct addrinfo* p; + int listenfd = -1; + for(p = ainfo; p; p = p->ai_next) { + if((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) + continue; + int yes = 1; + setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)); + if(bind(listenfd, p->ai_addr, p->ai_addrlen) < 0) { + close(listenfd); + listenfd = -1; + continue; + } + break; + } + freeaddrinfo(ainfo); + if(listenfd < 0) return -2; + if(listen(listenfd, SOMAXCONN) < 0) { + close(listenfd); + return -3; + } + server->fd = listenfd; + return 0; +} diff --git a/deployments/openvpn/third_party/microsocks/server.h b/deployments/openvpn/third_party/microsocks/server.h new file mode 100644 index 0000000..5acf664 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/server.h @@ -0,0 +1,49 @@ +#ifndef SERVER_H +#define SERVER_H + +#undef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include + +#pragma RcB2 DEP "server.c" + +union sockaddr_union { + struct sockaddr_in v4; + struct sockaddr_in6 v6; +}; + +#define SOCKADDR_UNION_AF(PTR) (PTR)->v4.sin_family + +#define SOCKADDR_UNION_LENGTH(PTR) ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET ) ? sizeof((PTR)->v4) : ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET6 ) ? sizeof((PTR)->v6) : 0 ) ) + +#define SOCKADDR_UNION_ADDRESS(PTR) ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET ) ? (void*) &(PTR)->v4.sin_addr : ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET6 ) ? (void*) &(PTR)->v6.sin6_addr : (void*) 0 ) ) + +#define SOCKADDR_UNION_PORT(PTR) ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET ) ? (PTR)->v4.sin_port : ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET6 ) ? (PTR)->v6.sin6_port : 0 ) ) + +struct client { + union sockaddr_union addr; + int fd; +}; + +struct server { + int fd; +}; + +int resolve(const char *host, unsigned short port, struct addrinfo** addr); +int resolve_sa(const char *host, unsigned short port, union sockaddr_union *res); +int bindtoip(int fd, union sockaddr_union *bindaddr); + +int server_waitclient(struct server *server, struct client* client); +int server_setup(struct server *server, const char* listenip, unsigned short port); + +#endif + diff --git a/deployments/openvpn/third_party/microsocks/sockssrv.c b/deployments/openvpn/third_party/microsocks/sockssrv.c new file mode 100644 index 0000000..d2b8bb5 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/sockssrv.c @@ -0,0 +1,482 @@ +/* + MicroSocks - multithreaded, small, efficient SOCKS5 server. + + Copyright (C) 2017 rofl0r. + + This is the successor of "rocksocks5", and it was written with + different goals in mind: + + - prefer usage of standard libc functions over homegrown ones + - no artificial limits + - do not aim for minimal binary size, but for minimal source code size, + and maximal readability, reusability, and extensibility. + + as a result of that, ipv4, dns, and ipv6 is supported out of the box + and can use the same code, while rocksocks5 has several compile time + defines to bring down the size of the resulting binary to extreme values + like 10 KB static linked when only ipv4 support is enabled. + + still, if optimized for size, *this* program when static linked against musl + libc is not even 50 KB. that's easily usable even on the cheapest routers. + +*/ + +#define _GNU_SOURCE +#include +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "server.h" +#include "sblist.h" + +/* timeout in microseconds on resource exhaustion to prevent excessive + cpu usage. */ +#ifndef FAILURE_TIMEOUT +#define FAILURE_TIMEOUT 64 +#endif + +#ifndef MAX +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + +#ifdef PTHREAD_STACK_MIN +#define THREAD_STACK_SIZE MAX(8*1024, PTHREAD_STACK_MIN) +#else +#define THREAD_STACK_SIZE 64*1024 +#endif + +#if defined(__APPLE__) +#undef THREAD_STACK_SIZE +#define THREAD_STACK_SIZE 64*1024 +#elif defined(__GLIBC__) || defined(__FreeBSD__) +#undef THREAD_STACK_SIZE +#define THREAD_STACK_SIZE 32*1024 +#endif + +static const char* auth_user; +static const char* auth_pass; +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}; + +enum socksstate { + SS_1_CONNECTED, + SS_2_NEED_AUTH, /* skipped if NO_AUTH method supported */ + SS_3_AUTHED, +}; + +enum authmethod { + AM_NO_AUTH = 0, + AM_GSSAPI = 1, + AM_USERNAME = 2, + AM_INVALID = 0xFF +}; + +enum errorcode { + EC_SUCCESS = 0, + EC_GENERAL_FAILURE = 1, + EC_NOT_ALLOWED = 2, + EC_NET_UNREACHABLE = 3, + EC_HOST_UNREACHABLE = 4, + EC_CONN_REFUSED = 5, + EC_TTL_EXPIRED = 6, + EC_COMMAND_NOT_SUPPORTED = 7, + EC_ADDRESSTYPE_NOT_SUPPORTED = 8, +}; + +struct thread { + pthread_t pt; + struct client client; + enum socksstate state; + volatile int done; +}; + +#ifndef CONFIG_LOG +#define CONFIG_LOG 1 +#endif +#if CONFIG_LOG +/* 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(...) dprintf(2, __VA_ARGS__) +#else +static void dolog(const char* fmt, ...) { } +#endif + +static struct addrinfo* addr_choose(struct addrinfo* list, union sockaddr_union* bind_addr) { + int af = SOCKADDR_UNION_AF(bind_addr); + if(af == AF_UNSPEC) return list; + struct addrinfo* p; + for(p=list; p; p=p->ai_next) + if(p->ai_family == af) return p; + return list; +} + +static int connect_socks_target(unsigned char *buf, size_t n, struct client *client) { + 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 */ + + int af = AF_INET; + size_t minlen = 4 + 4 + 2, l; + char namebuf[256]; + struct addrinfo* remote; + + switch(buf[3]) { + case 4: /* ipv6 */ + af = AF_INET6; + minlen = 4 + 2 + 16; + /* fall through */ + case 1: /* ipv4 */ + if(n < minlen) return -EC_GENERAL_FAILURE; + if(namebuf != inet_ntop(af, buf+4, namebuf, sizeof namebuf)) + return -EC_GENERAL_FAILURE; /* malformed or too long addr */ + break; + case 3: /* dns name */ + l = buf[4]; + minlen = 4 + 2 + l + 1; + if(n < 4 + 2 + l + 1) return -EC_GENERAL_FAILURE; + memcpy(namebuf, buf+4+1, l); + namebuf[l] = 0; + break; + default: + return -EC_ADDRESSTYPE_NOT_SUPPORTED; + } + unsigned short port; + port = (buf[minlen-2] << 8) | buf[minlen-1]; + /* 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(fd == -1) { + eval_errno: + if(fd != -1) close(fd); + freeaddrinfo(remote); + switch(errno) { + case ETIMEDOUT: + return -EC_TTL_EXPIRED; + case EPROTOTYPE: + case EPROTONOSUPPORT: + case EAFNOSUPPORT: + return -EC_ADDRESSTYPE_NOT_SUPPORTED; + case ECONNREFUSED: + return -EC_CONN_REFUSED; + case ENETDOWN: + case ENETUNREACH: + return -EC_NET_UNREACHABLE; + case EHOSTUNREACH: + return -EC_HOST_UNREACHABLE; + case EBADF: + default: + perror("socket/connect"); + return -EC_GENERAL_FAILURE; + } + } + if(SOCKADDR_UNION_AF(&bind_addr) == raddr->ai_family && + bindtoip(fd, &bind_addr) == -1) + goto eval_errno; + if(connect(fd, raddr->ai_addr, raddr->ai_addrlen) == -1) + goto eval_errno; + + freeaddrinfo(remote); + if(CONFIG_LOG) { + char clientname[256]; + 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); + } + return fd; +} + +static int is_authed(union sockaddr_union *client, union sockaddr_union *authedip) { + int af = SOCKADDR_UNION_AF(authedip); + if(af == SOCKADDR_UNION_AF(client)) { + size_t cmpbytes = af == AF_INET ? 4 : 16; + void *cmp1 = SOCKADDR_UNION_ADDRESS(client); + void *cmp2 = SOCKADDR_UNION_ADDRESS(authedip); + if(!memcmp(cmp1, cmp2, cmpbytes)) return 1; + } + return 0; +} + +static int is_in_authed_list(union sockaddr_union *caddr) { + size_t i; + for(i=0;i= n ) return AM_INVALID; + int n_methods = buf[idx]; + idx++; + while(idx < n && n_methods > 0) { + if(buf[idx] == AM_NO_AUTH) { + if(!auth_user) return AM_NO_AUTH; + else if(auth_ips) { + int authed = 0; + if(pthread_rwlock_rdlock(&auth_ips_lock) == 0) { + authed = is_in_authed_list(&client->addr); + pthread_rwlock_unlock(&auth_ips_lock); + } + if(authed) return AM_NO_AUTH; + } + } else if(buf[idx] == AM_USERNAME) { + if(auth_user) return AM_USERNAME; + } + idx++; + n_methods--; + } + return AM_INVALID; +} + +static void send_auth_response(int fd, int version, enum authmethod meth) { + unsigned char buf[2]; + buf[0] = version; + buf[1] = meth; + write(fd, buf, 2); +} + +static void send_error(int fd, enum errorcode ec) { + /* position 4 contains ATYP, the address type, which is the same as used in the connect + request. we're lazy and return always IPV4 address type in errors. */ + char buf[10] = { 5, ec, 0, 1 /*AT_IPV4*/, 0,0,0,0, 0,0 }; + write(fd, buf, 10); +} + +static void copyloop(int fd1, int fd2) { + struct pollfd fds[2] = { + [0] = {.fd = fd1, .events = POLLIN}, + [1] = {.fd = fd2, .events = POLLIN}, + }; + + while(1) { + /* 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. */ + switch(poll(fds, 2, 60*15*1000)) { + case 0: + return; + case -1: + if(errno == EINTR || errno == EAGAIN) continue; + else perror("poll"); + return; + } + int infd = (fds[0].revents & POLLIN) ? fd1 : fd2; + int outfd = infd == fd2 ? fd1 : fd2; + char buf[1024]; + ssize_t sent = 0, n = read(infd, buf, sizeof buf); + if(n <= 0) return; + while(sent < n) { + ssize_t m = write(outfd, buf+sent, n-sent); + if(m < 0) return; + sent += m; + } + } +} + +static enum errorcode check_credentials(unsigned char* buf, size_t n) { + if(n < 5) return EC_GENERAL_FAILURE; + if(buf[0] != 1) return EC_GENERAL_FAILURE; + unsigned ulen, plen; + ulen=buf[1]; + if(n < 2 + ulen + 2) return EC_GENERAL_FAILURE; + plen=buf[2+ulen]; + if(n < 2 + ulen + 1 + plen) return EC_GENERAL_FAILURE; + char user[256], pass[256]; + memcpy(user, buf+2, ulen); + memcpy(pass, buf+2+ulen+1, plen); + user[ulen] = 0; + pass[plen] = 0; + if(!strcmp(user, auth_user) && !strcmp(pass, auth_pass)) return EC_SUCCESS; + return EC_NOT_ALLOWED; +} + +static void* clientthread(void *data) { + struct thread *t = data; + t->state = SS_1_CONNECTED; + unsigned char buf[1024]; + ssize_t n; + int ret; + int remotefd = -1; + enum authmethod am; + while((n = recv(t->client.fd, buf, sizeof buf, 0)) > 0) { + switch(t->state) { + case SS_1_CONNECTED: + am = check_auth_method(buf, n, &t->client); + if(am == AM_NO_AUTH) t->state = SS_3_AUTHED; + else if (am == AM_USERNAME) t->state = SS_2_NEED_AUTH; + send_auth_response(t->client.fd, 5, am); + if(am == AM_INVALID) goto breakloop; + break; + case SS_2_NEED_AUTH: + ret = check_credentials(buf, n); + send_auth_response(t->client.fd, 1, ret); + if(ret != EC_SUCCESS) + goto breakloop; + t->state = SS_3_AUTHED; + if(auth_ips && !pthread_rwlock_wrlock(&auth_ips_lock)) { + if(!is_in_authed_list(&t->client.addr)) + add_auth_ip(&t->client.addr); + pthread_rwlock_unlock(&auth_ips_lock); + } + break; + case SS_3_AUTHED: + ret = connect_socks_target(buf, n, &t->client); + if(ret < 0) { + send_error(t->client.fd, ret*-1); + goto breakloop; + } + remotefd = ret; + send_error(t->client.fd, EC_SUCCESS); + copyloop(t->client.fd, remotefd); + goto breakloop; + + } + } +breakloop: + + if(remotefd != -1) + close(remotefd); + + close(t->client.fd); + t->done = 1; + + return 0; +} + +static void collect(sblist *threads) { + size_t i; + for(i=0;idone) { + pthread_join(thread->pt, 0); + sblist_delete(threads, i); + free(thread); + } else + i++; + } +} + +static int usage(void) { + dprintf(2, + "MicroSocks SOCKS5 Server\n" + "------------------------\n" + "usage: microsocks -1 -i listenip -p port -u user -P password -b bindaddr\n" + "all arguments are optional.\n" + "by default listenip is 0.0.0.0 and port 1080.\n\n" + "option -b specifies which ip outgoing connections are bound to\n" + "option -1 activates auth_once mode: once a specific ip address\n" + "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" + ); + return 1; +} + +/* prevent username and password from showing up in top. */ +static void zero_arg(char *s) { + size_t i, l = strlen(s); + for(i=0;idone = 0; + if(server_waitclient(&s, &c)) { + dolog("failed to accept connection\n"); + free(curr); + usleep(FAILURE_TIMEOUT); + continue; + } + curr->client = c; + if(!sblist_add(threads, &curr)) { + close(curr->client.fd); + free(curr); + oom: + dolog("rejecting connection due to OOM\n"); + usleep(FAILURE_TIMEOUT); /* prevent 100% CPU usage in OOM situation */ + continue; + } + pthread_attr_t *a = 0, attr; + if(pthread_attr_init(&attr) == 0) { + a = &attr; + pthread_attr_setstacksize(a, THREAD_STACK_SIZE); + } + if(pthread_create(&curr->pt, a, clientthread, curr) != 0) + dolog("pthread_create failed. OOM?\n"); + if(a) pthread_attr_destroy(&attr); + } +} diff --git a/internal/infrastructure/external/rongxing/curl_helper.go b/internal/infrastructure/external/rongxing/curl_helper.go index 49c3832..172a043 100644 --- a/internal/infrastructure/external/rongxing/curl_helper.go +++ b/internal/infrastructure/external/rongxing/curl_helper.go @@ -5,14 +5,21 @@ import ( ) // generateCurlCommand 生成可直接复现的 curl 命令,便于联调排查。 -func generateCurlCommand(method, url string, headers map[string]string, body string) string { +// proxyURL 非空时附加 -x,便于确认线上是否应走 SOCKS。 +func generateCurlCommand(method, requestURL string, headers map[string]string, body, proxyURL string) string { var cmd strings.Builder cmd.WriteString("curl -X ") cmd.WriteString(method) cmd.WriteString(" '") - cmd.WriteString(url) + cmd.WriteString(requestURL) cmd.WriteString("'") + if p := strings.TrimSpace(proxyURL); p != "" { + cmd.WriteString(" \\\n -x '") + cmd.WriteString(escapeSingleQuotes(p)) + cmd.WriteString("'") + } + for key, value := range headers { cmd.WriteString(" \\\n -H '") cmd.WriteString(key) diff --git a/internal/infrastructure/external/rongxing/rongxing_service.go b/internal/infrastructure/external/rongxing/rongxing_service.go index 229681d..8fe0d82 100644 --- a/internal/infrastructure/external/rongxing/rongxing_service.go +++ b/internal/infrastructure/external/rongxing/rongxing_service.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "net/url" + "os" "strings" "sync" "time" @@ -163,7 +164,7 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m "Content-Type": "application/json", headerDmsToken: token, } - curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr) + curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr, s.config.Proxy) req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes)) if err != nil { @@ -298,7 +299,7 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri bodyStr := string(bodyBytes) headers := map[string]string{"Content-Type": "application/json"} - curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr) + curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr, s.config.Proxy) req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes)) if err != nil { @@ -356,7 +357,7 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) { resp, err := s.client.Do(req) if err != nil { - return nil, 0, err + return nil, 0, s.wrapTransportError(req.URL.String(), err) } defer resp.Body.Close() @@ -367,6 +368,48 @@ func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) { return body, resp.StatusCode, nil } +// wrapTransportError 把超时/拒连/代理失败等包装成可读诊断,便于区分「联不通」原因。 +func (s *RongxingService) wrapTransportError(targetURL string, err error) error { + diag := classifyTransportError(err) + proxyMode := strings.TrimSpace(s.config.Proxy) + if proxyMode == "" { + proxyMode = "(直连,未配置 proxy)" + } + return fmt.Errorf( + "戎行 HTTP 失败 target=%s proxy=%s diagnosis=%s cause=%w", + targetURL, proxyMode, diag, err, + ) +} + +func classifyTransportError(err error) string { + if err == nil { + return "unknown" + } + msg := err.Error() + + switch { + case os.IsTimeout(err) || errors.Is(err, context.DeadlineExceeded) || + strings.Contains(msg, "Client.Timeout") || strings.Contains(msg, "deadline exceeded"): + return "请求超时(未在 timeout 内收到响应头;可能:目标 192.168.3.43:7007 不可达、VPN/SOCKS 未转发、或服务无响应)" + case strings.Contains(msg, "connection refused"): + return "连接被拒绝(端口未监听或代理/目标拒绝)" + case strings.Contains(msg, "no such host") || strings.Contains(msg, "lookup"): + return "DNS/主机名解析失败(检查 rongxing-vpn 服务名或目标域名)" + case strings.Contains(msg, "network is unreachable") || strings.Contains(msg, "no route to host"): + return "网络不可达(无路由;直连内网 IP 时常见于未走 VPN/代理)" + case strings.Contains(msg, "i/o timeout") || strings.Contains(msg, "TLS handshake timeout"): + return "传输层超时(链路通但握手/读写超时)" + case strings.Contains(msg, "proxy") || strings.Contains(msg, "socks"): + return "代理链路异常(检查 socks5://rongxing-vpn:1080 与 VPN 容器)" + } + + var netErr net.Error + if errors.As(err, &netErr) && netErr.Timeout() { + return "网络超时" + } + return "其他网络错误(详见 cause)" +} + func (s *RongxingService) validateConfig() error { if s.config.BaseURL == "" { return errors.New("戎行 url 未配置") @@ -429,12 +472,35 @@ func (s *RongxingService) logErrorWithCurl(transactionID, apiKey string, err err if s.logger == nil { return } + proxyMode := strings.TrimSpace(s.config.Proxy) + if proxyMode == "" { + proxyMode = "(直连,未配置 proxy)" + } s.logger.LogErrorWithFields("rongxing API错误", zap.String("transaction_id", transactionID), zap.String("api_code", apiKey), + zap.String("base_url", s.config.BaseURL), + zap.String("proxy", proxyMode), + zap.String("diagnosis", extractDiagnosis(err)), zap.Error(err), zap.Any("params", payload), zap.String("curl", curlCmd), zap.String("response_body", respBody), ) } + +func extractDiagnosis(err error) string { + if err == nil { + return "" + } + const marker = "diagnosis=" + msg := err.Error() + if i := strings.Index(msg, marker); i >= 0 { + rest := msg[i+len(marker):] + if j := strings.Index(rest, " cause="); j >= 0 { + return rest[:j] + } + return rest + } + return classifyTransportError(err) +} diff --git a/internal/infrastructure/external/rongxing/transport_error_test.go b/internal/infrastructure/external/rongxing/transport_error_test.go new file mode 100644 index 0000000..985c44b --- /dev/null +++ b/internal/infrastructure/external/rongxing/transport_error_test.go @@ -0,0 +1,43 @@ +package rongxing + +import ( + "context" + "errors" + "strings" + "testing" + "time" +) + +func TestClassifyTransportError_Timeout(t *testing.T) { + err := errors.New(`Post "http://192.168.3.43:7007/auth/login": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`) + got := classifyTransportError(err) + if !strings.Contains(got, "请求超时") { + t.Fatalf("got %q", got) + } +} + +func TestClassifyTransportError_Deadline(t *testing.T) { + got := classifyTransportError(context.DeadlineExceeded) + if !strings.Contains(got, "请求超时") { + t.Fatalf("got %q", got) + } +} + +func TestWrapTransportError_IncludesProxy(t *testing.T) { + svc, err := NewRongxingService(serviceConfig{ + BaseURL: "http://192.168.3.43:7007", + Timeout: time.Second, + Proxy: "socks5://rongxing-vpn:1080", + }, nil) + if err != nil { + t.Fatal(err) + } + wrapped := svc.wrapTransportError("http://192.168.3.43:7007/auth/login", context.DeadlineExceeded) + msg := wrapped.Error() + if !strings.Contains(msg, "proxy=socks5://rongxing-vpn:1080") { + t.Fatalf("missing proxy in error: %s", msg) + } + if !strings.Contains(msg, "diagnosis=") { + t.Fatalf("missing diagnosis in error: %s", msg) + } +}