From 661078c3bff1d244ef2db218dcfb677a168fd0dd Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 28 May 2026 20:59:21 +0800 Subject: [PATCH] Show SOCKS5 proxy exit IP --- app.py | 39 +++++++++++++++++++++++++++++++++++---- static/index.html | 2 +- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 4e87445..d99cc3c 100644 --- a/app.py +++ b/app.py @@ -324,13 +324,13 @@ class Notifier: return {"channel": name, "ok": False, "error": repr(e)} -async def socks5_connect(proxy: dict[str, Any], timeout: float) -> None: +async def socks5_open_connection(proxy: dict[str, Any], timeout: float, target_host: str | None = None, target_port: int | None = None) -> tuple[asyncio.StreamReader, asyncio.StreamWriter]: host = proxy["host"] port = int(proxy["port"]) username = proxy.get("username") or "" password = proxy.get("password") or "" - target_host = proxy.get("target_host", "1.1.1.1") - target_port = int(proxy.get("target_port", 443)) + target_host = str(target_host or proxy.get("target_host") or "www.baidu.com") + target_port = int(target_port or proxy.get("target_port", 443)) reader, writer = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=timeout) try: methods = [0x00] @@ -360,6 +360,33 @@ async def socks5_connect(proxy: dict[str, Any], timeout: float) -> None: elif atyp == 0x03: await reader.readexactly((await reader.readexactly(1))[0]) elif atyp == 0x04: await reader.readexactly(16) await reader.readexactly(2) + return reader, writer + except Exception: + writer.close() + with contextlib.suppress(Exception): await writer.wait_closed() + raise + + +async def socks5_connect(proxy: dict[str, Any], timeout: float) -> None: + reader, writer = await socks5_open_connection(proxy, timeout) + writer.close() + with contextlib.suppress(Exception): await writer.wait_closed() + + +async def get_exit_ip_via_socks(proxy: dict[str, Any], timeout: float) -> str | None: + """Fetch the public egress IP through the SOCKS5 proxy without failing health checks.""" + reader, writer = await socks5_open_connection(proxy, timeout, "api.ipify.org", 80) + try: + req = b"GET /?format=text HTTP/1.1\r\nHost: api.ipify.org\r\nUser-Agent: socks5-monitor/1.0\r\nConnection: close\r\n\r\n" + writer.write(req); await writer.drain() + raw = await asyncio.wait_for(reader.read(8192), timeout=timeout) + text = raw.decode("utf-8", "replace") + body = text.split("\r\n\r\n", 1)[-1].strip() + m = re.search(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", body) + if m: + return m.group(0) + m = re.search(r"\b[0-9a-fA-F:]{3,}\b", body) + return m.group(0) if m else None finally: writer.close() with contextlib.suppress(Exception): await writer.wait_closed() @@ -370,7 +397,11 @@ async def check_proxy(proxy: dict[str, Any]) -> tuple[bool, int | None, str | No start = time.perf_counter() try: await socks5_connect(proxy, timeout) - return True, int((time.perf_counter() - start) * 1000), None, None + latency = int((time.perf_counter() - start) * 1000) + exit_ip = None + with contextlib.suppress(Exception): + exit_ip = await get_exit_ip_via_socks(proxy, timeout) + return True, latency, None, exit_ip except Exception as e: return False, int((time.perf_counter() - start) * 1000), str(e), None diff --git a/static/index.html b/static/index.html index 6e72515..24d5725 100644 --- a/static/index.html +++ b/static/index.html @@ -15,7 +15,7 @@ let DATA=null, editingProxy=null, editingChannel=null; const $=id=>document.getE document.querySelectorAll('.tab').forEach(b=>b.onclick=()=>{closeDrawer();document.querySelectorAll('.tab,.section').forEach(x=>x.classList.remove('active'));b.classList.add('active');$(b.dataset.tab).classList.add('active')}) function statusPill(s){let c=s||'unknown';return `${esc(c)}`} async function refresh(){DATA=await api('/api/status'); $('now').textContent='当前时间:'+DATA.now_text; let ps=DATA.proxies, up=ps.filter(x=>x.state.status==='up').length, down=ps.filter(x=>x.state.status==='down').length; $('mTotal').textContent=ps.length;$('mUp').textContent=up;$('mDown').textContent=down;$('mChan').textContent=(DATA.notifications.channels||[]).length; renderSettings(); renderProxies(); renderChannels(); renderLogs()} -function renderProxies(){let cards='', list=''; for(const it of DATA.proxies){let p=it.proxy,s=it.state; let recent=(it.recent||[]).slice(0,5).map(r=>`${ts(r.ts)}${r.ok?'OK':'FAIL'}${r.latency_ms??'-'}ms${esc(r.error||'')}`).join(''); let color=s.status==='up'?'green':s.status==='down'?'pink':'blue'; let block=`

${esc(p.name||p.id)}

${esc(p.host)}:${esc(p.port)} → ${esc(p.target_host)}:${esc(p.target_port)} · ${esc(p.interval_seconds||DATA.settings.interval_seconds)}s
${statusPill(s.status)}
最后检测
${ts(s.last_ts)}
延迟
${s.latency_ms??'-'} ms
失败/成功
${s.fail_count} / ${s.ok_count}
错误
${esc(s.error||'-')}
${recent}
时间结果延迟错误
`; cards+=block; list+=block } $('proxyCards').innerHTML=cards||'
还没有节点,点击新增节点。
'; $('proxyList').innerHTML=list||'

暂无节点

'} +function renderProxies(){let cards='', list=''; for(const it of DATA.proxies){let p=it.proxy,s=it.state; let recent=(it.recent||[]).slice(0,5).map(r=>`${ts(r.ts)}${r.ok?'OK':'FAIL'}${r.latency_ms??'-'}ms${esc(r.exit_ip||'-')}${esc(r.error||'')}`).join(''); let color=s.status==='up'?'green':s.status==='down'?'pink':'blue'; let block=`

${esc(p.name||p.id)}

${esc(p.host)}:${esc(p.port)} → ${esc(p.target_host)}:${esc(p.target_port)} · ${esc(p.interval_seconds||DATA.settings.interval_seconds)}s
${statusPill(s.status)}
最后检测
${ts(s.last_ts)}
延迟
${s.latency_ms??'-'} ms
代理出口 IP
${esc(s.exit_ip||'-')}
失败/成功
${s.fail_count} / ${s.ok_count}
错误
${esc(s.error||'-')}
${recent}
时间结果延迟出口 IP错误
`; cards+=block; list+=block } $('proxyCards').innerHTML=cards||'
还没有节点,点击新增节点。
'; $('proxyList').innerHTML=list||'

暂无节点

'} function channelSummary(c){let x=cfg(c); if(c.type==='bark')return x.key?'Key 已配置':'Key 未配置'; if(c.type==='pushplus')return x.token?'Token 已配置':'Token 未配置'; if(c.type==='autman')return x.access_token?'Token 已配置':'Token 未配置'; return x.url||''} function renderSettings(){let s=DATA.settings||{}; if($('sTarget')){$('sTarget').textContent=(s.target_host||'-')+':'+(s.target_port||'-');$('sInterval').textContent=s.interval_seconds||'-';$('sTimeout').textContent=s.timeout_seconds||'-';$('sThreshold').textContent='失败 '+(s.fail_threshold||'-')+' / 恢复 '+(s.recover_threshold||'-')}} function renderChannels(){let html=''; for(const c of DATA.notifications.channels||[]){html+=`

${esc(c.name)}

${esc(c.type)}${c.enabled?'启用':'停用'}
${esc(channelSummary(c))}
`} $('channelList').innerHTML=html||'

暂无通知渠道

'}