procproxy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- procproxy-0.1.0.dist-info/METADATA +489 -0
- procproxy-0.1.0.dist-info/RECORD +45 -0
- procproxy-0.1.0.dist-info/WHEEL +5 -0
- procproxy-0.1.0.dist-info/entry_points.txt +2 -0
- procproxy-0.1.0.dist-info/licenses/LICENSE +21 -0
- procproxy-0.1.0.dist-info/top_level.txt +1 -0
- proxycore/__init__.py +7 -0
- proxycore/__main__.py +65 -0
- proxycore/api.py +226 -0
- proxycore/backend/__init__.py +49 -0
- proxycore/backend/base.py +159 -0
- proxycore/backend/null.py +51 -0
- proxycore/backend/windivert/__init__.py +657 -0
- proxycore/backend/windivert/filter.py +123 -0
- proxycore/backend/windivert/pidmap.py +289 -0
- proxycore/backend/windivert/pkt.py +424 -0
- proxycore/canproxy.py +229 -0
- proxycore/checkroute.py +247 -0
- proxycore/cli.py +605 -0
- proxycore/config.py +1010 -0
- proxycore/dns/__init__.py +46 -0
- proxycore/dns/cache.py +209 -0
- proxycore/dns/modes.py +451 -0
- proxycore/dns/udp.py +606 -0
- proxycore/forward.py +572 -0
- proxycore/ipc.py +637 -0
- proxycore/log.py +298 -0
- proxycore/pathsetup.py +112 -0
- proxycore/probe.py +168 -0
- proxycore/proc.py +342 -0
- proxycore/rules.py +730 -0
- proxycore/runtime.py +654 -0
- proxycore/service/__init__.py +536 -0
- proxycore/service/__main__.py +124 -0
- proxycore/tunnel/__init__.py +8 -0
- proxycore/tunnel/relay.py +393 -0
- proxycore/tunnel/sessions.py +315 -0
- proxycore/tunnel/sink.py +325 -0
- proxycore/types.py +165 -0
- proxycore/upstream/__init__.py +52 -0
- proxycore/upstream/base.py +150 -0
- proxycore/upstream/chain.py +92 -0
- proxycore/upstream/http_connect.py +148 -0
- proxycore/upstream/socks4.py +106 -0
- proxycore/upstream/socks5.py +266 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""路线 A 的 WinDivert 过滤器表达式构造。
|
|
2
|
+
|
|
3
|
+
一条表达式要同时满足四件事,漏一条就会成环或卡死:
|
|
4
|
+
|
|
5
|
+
* 抓所有出向 TCP,不只 SYN。整条连接两个方向都要改写,只抓 SYN 会在握手第二包断。
|
|
6
|
+
* 排除回环。改道后应用连的是 127.0.0.1:<sink>,不排除会自我循环。
|
|
7
|
+
* 排除 sink 端口,回环之外再加一道。
|
|
8
|
+
* 排除上游代理和本地 DNS 的 IP。拦截器自己连上游会被再次捕获,同样成环。
|
|
9
|
+
|
|
10
|
+
地址族:ip.DstAddr 只匹配 IPv4,ipv6.DstAddr 才匹配 IPv6(没有 ipv4. 前缀)。
|
|
11
|
+
官方文档明确"字段与报文不相关时该 test 判为 false",所以含 ip.DstAddr 的表达式
|
|
12
|
+
永远不匹配 IPv6 报文。要覆盖 v6 必须另开一个句柄、用 ipv6.* 写第二条表达式,
|
|
13
|
+
本函数不试图在一条表达式里同时管两个族。
|
|
14
|
+
|
|
15
|
+
上游地址如果是域名,解析结果会变,而改过滤表达式要重开句柄。所以静态场景由
|
|
16
|
+
build_filter() 排除,用户态在每次 redirect() 前再判一次。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ipaddress
|
|
22
|
+
from collections.abc import Iterable
|
|
23
|
+
|
|
24
|
+
LOOPBACK_V4 = "127.0.0.1"
|
|
25
|
+
LOOPBACK_V6 = "::1"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _normalize_ips(values: Iterable[str], *, version: int | None = None) -> list[str]:
|
|
29
|
+
"""过滤出合法的 IP 字面量。
|
|
30
|
+
|
|
31
|
+
`version` 给出时只保留该地址族:WinDivert 的字段命名空间是按族分的
|
|
32
|
+
(`ip.*` 对 IPv4、`ipv6.*` 对 IPv6),把 v4 地址写进 v6 表达式等于没写。
|
|
33
|
+
"""
|
|
34
|
+
out: list[str] = []
|
|
35
|
+
for value in values:
|
|
36
|
+
value = str(value).strip()
|
|
37
|
+
if not value:
|
|
38
|
+
continue
|
|
39
|
+
# 只接受 IP 字面量;域名不能出现在 WinDivert 表达式里
|
|
40
|
+
try:
|
|
41
|
+
parsed = ipaddress.ip_address(value)
|
|
42
|
+
except ValueError:
|
|
43
|
+
continue
|
|
44
|
+
if version is not None and parsed.version != version:
|
|
45
|
+
continue
|
|
46
|
+
if value not in out:
|
|
47
|
+
out.append(value)
|
|
48
|
+
return out
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def build_filter(
|
|
52
|
+
*,
|
|
53
|
+
sink_addr: str = "",
|
|
54
|
+
sink_port: int = 19080,
|
|
55
|
+
exclude_ips: Iterable[str] = (),
|
|
56
|
+
ipv6: bool = False,
|
|
57
|
+
sniffer: bool = False,
|
|
58
|
+
) -> str:
|
|
59
|
+
"""构造过滤器表达式。
|
|
60
|
+
|
|
61
|
+
`sink_addr` 留空时按 `ipv6` 取默认回环地址(v4 用 `127.0.0.1`、v6 用 `::1`)。
|
|
62
|
+
`sniffer=True` 加 `sniff`:只观察不拦截,`--dry-run` 用。
|
|
63
|
+
|
|
64
|
+
`ipv6=True` 产出的是给第二个句柄用的那条表达式(见模块 docstring 的地址族说明)。
|
|
65
|
+
两个分支互不排除对方的回环:v4 表达式里写 `ipv6.DstAddr != ::1` 恒真,没有意义,
|
|
66
|
+
反过来同理。代价是 `ipv6=False` 时 v6 流量整体静默漏过,调用方必须明确这一点。
|
|
67
|
+
"""
|
|
68
|
+
family = 6 if ipv6 else 4
|
|
69
|
+
ns = "ipv6" if ipv6 else "ip"
|
|
70
|
+
loopback = LOOPBACK_V6 if ipv6 else LOOPBACK_V4
|
|
71
|
+
|
|
72
|
+
sink_addr = sink_addr or loopback
|
|
73
|
+
clauses: list[str] = ["tcp", f"(outbound or tcp.SrcPort == {int(sink_port)})"]
|
|
74
|
+
clauses.append(f"tcp.DstPort != {int(sink_port)}")
|
|
75
|
+
|
|
76
|
+
# 本族回环地址永不代理,但 sink 的应答必须放行。
|
|
77
|
+
#
|
|
78
|
+
# 改道后整条连接都在回环里流动:
|
|
79
|
+
# 正向 127.0.0.1:客户端端口 → 127.0.0.1:<sink>
|
|
80
|
+
# 反向 127.0.0.1:<sink> → 127.0.0.1:客户端端口
|
|
81
|
+
# 反向包的目的地址也是回环,整段排除会连 sink 的应答一起丢掉。症状是改道日志
|
|
82
|
+
# 正常、sink 也 accept 了,客户端却收不到响应而超时。
|
|
83
|
+
# 所以只放行源端口等于 sink 端口的那一类:那一定是 sink 的应答,
|
|
84
|
+
# 不可能是应用自己发起的回环连接。
|
|
85
|
+
clauses.append(f"({ns}.DstAddr != {loopback} or tcp.SrcPort == {int(sink_port)})")
|
|
86
|
+
|
|
87
|
+
# sink 自己也要排除(sink 不在回环上时上面那条盖不住)
|
|
88
|
+
if sink_addr and sink_addr != loopback:
|
|
89
|
+
clauses.append(f"{ns}.DstAddr != {sink_addr}")
|
|
90
|
+
|
|
91
|
+
for addr in _normalize_ips(exclude_ips, version=family):
|
|
92
|
+
if addr in (sink_addr, loopback):
|
|
93
|
+
continue # 上面已写过,再写一遍会重复
|
|
94
|
+
clauses.append(f"{ns}.DstAddr != {addr}")
|
|
95
|
+
|
|
96
|
+
expr = " and ".join(clauses)
|
|
97
|
+
if sniffer:
|
|
98
|
+
expr += " and sniff"
|
|
99
|
+
return expr
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def upstream_ips_from_config(config: object) -> list[str]:
|
|
103
|
+
"""从配置里取上游地址(用于过滤器排除与用户态二次校验)。"""
|
|
104
|
+
profile = getattr(config, "profile", None)
|
|
105
|
+
upstreams = getattr(profile, "upstreams", {}) or {}
|
|
106
|
+
out: list[str] = []
|
|
107
|
+
for upstream in upstreams.values():
|
|
108
|
+
host = str(getattr(upstream, "host", "")).strip()
|
|
109
|
+
if not host:
|
|
110
|
+
continue
|
|
111
|
+
try:
|
|
112
|
+
ipaddress.ip_address(host)
|
|
113
|
+
except ValueError:
|
|
114
|
+
continue # 域名:需要先解析,交给用户态判断
|
|
115
|
+
if host not in out:
|
|
116
|
+
out.append(host)
|
|
117
|
+
dns = str(getattr(profile, "dns_local_server", "") or "").strip()
|
|
118
|
+
if dns and dns not in out:
|
|
119
|
+
out.append(dns)
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__all__ = ["build_filter", "upstream_ips_from_config"]
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""路线 A · `local_port → PID` 查表(等价于内核 ALE 层拿到的 `PROCESS_ID`)。
|
|
2
|
+
|
|
3
|
+
`GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL)` 在 SYN 发出后通常能查到
|
|
4
|
+
该连接(状态 `SYN_SENT=3`),但存在窗口。工程处理:
|
|
5
|
+
|
|
6
|
+
1. 先按 `local_port` 精确匹配(并校验 remote 一致);
|
|
7
|
+
2. 不中 → 最多 3 次、每次 3ms 后刷新重试;
|
|
8
|
+
3. 仍不中 → 按 `(raddr, rport)` 反查兜底(同机同目标并发少时可用);
|
|
9
|
+
4. 返回 0 表示未知 → 核心按 `on_unknown_pid` 策略处理并记 WARN。
|
|
10
|
+
|
|
11
|
+
禁止用 `GetTcpTable`(不带 PID 的版本), 明令禁止项。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import socket
|
|
17
|
+
import struct
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from ...log import EventLogger, get_logger
|
|
24
|
+
|
|
25
|
+
LOG = EventLogger(get_logger("proxycore.backend.windivert.pidmap"))
|
|
26
|
+
|
|
27
|
+
AF_INET = 2
|
|
28
|
+
AF_INET6 = 23
|
|
29
|
+
TCP_TABLE_OWNER_PID_ALL = 5
|
|
30
|
+
UDP_TABLE_OWNER_PID = 1
|
|
31
|
+
|
|
32
|
+
# MIB_TCPROW_OWNER_PID: state, localAddr, localPort, remoteAddr, remotePort, pid (每项 4 字节)
|
|
33
|
+
TCP_ROW_SIZE = 24
|
|
34
|
+
TCP6_ROW_SIZE = 56 # + scope ids 与 16 字节地址
|
|
35
|
+
UDP_ROW_SIZE = 12
|
|
36
|
+
UDP6_ROW_SIZE = 28
|
|
37
|
+
|
|
38
|
+
STATE_SYN_SENT = 3
|
|
39
|
+
STATE_ESTABLISHED = 5
|
|
40
|
+
|
|
41
|
+
DEFAULT_RETRIES = 3
|
|
42
|
+
DEFAULT_RETRY_DELAY_S = 0.003
|
|
43
|
+
DEFAULT_TTL_S = 0.25
|
|
44
|
+
|
|
45
|
+
_UNKNOWN_PID = 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class TcpRow:
|
|
50
|
+
state: int
|
|
51
|
+
local_port: int
|
|
52
|
+
local_addr: str
|
|
53
|
+
remote_addr: str
|
|
54
|
+
remote_port: int
|
|
55
|
+
pid: int
|
|
56
|
+
is_v6: bool = False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PortPidTable:
|
|
60
|
+
"""带 TTL 的 `local_port → TcpRow` 快照表。
|
|
61
|
+
|
|
62
|
+
只用于首次建表与兜底反查;热点路径是"命中即返回"。
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, *, ttl_s: float = DEFAULT_TTL_S) -> None:
|
|
66
|
+
self.ttl_s = ttl_s
|
|
67
|
+
self._lock = threading.RLock()
|
|
68
|
+
self._by_port: dict[tuple[bool, int], TcpRow] = {}
|
|
69
|
+
self._rows: list[TcpRow] = []
|
|
70
|
+
self._refreshed_at = 0.0
|
|
71
|
+
self.refresh_count = 0
|
|
72
|
+
self.last_error = ""
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------ 刷新 -----
|
|
75
|
+
def refresh(self, *, force: bool = False) -> None:
|
|
76
|
+
with self._lock:
|
|
77
|
+
now = time.monotonic()
|
|
78
|
+
if not force and self._rows and now - self._refreshed_at < self.ttl_s:
|
|
79
|
+
return
|
|
80
|
+
rows: list[TcpRow] = []
|
|
81
|
+
try:
|
|
82
|
+
rows.extend(_read_tcp_table(AF_INET))
|
|
83
|
+
rows.extend(_read_tcp_table(AF_INET6))
|
|
84
|
+
except OSError as exc:
|
|
85
|
+
self.last_error = str(exc)
|
|
86
|
+
LOG.warn(
|
|
87
|
+
"pidmap_refresh_failed",
|
|
88
|
+
error=str(exc),
|
|
89
|
+
hint="需要管理员权限;路线 A 必须有管理员才能查全表",
|
|
90
|
+
)
|
|
91
|
+
return
|
|
92
|
+
self._rows = rows
|
|
93
|
+
self._by_port = {(row.is_v6, row.local_port): row for row in rows}
|
|
94
|
+
self._refreshed_at = now
|
|
95
|
+
self.refresh_count += 1
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------ 查询 -----
|
|
98
|
+
def lookup(
|
|
99
|
+
self,
|
|
100
|
+
local_port: int,
|
|
101
|
+
remote_addr: str | None = None,
|
|
102
|
+
remote_port: int | None = None,
|
|
103
|
+
*,
|
|
104
|
+
is_v6: bool = False,
|
|
105
|
+
retries: int = DEFAULT_RETRIES,
|
|
106
|
+
retry_delay_s: float = DEFAULT_RETRY_DELAY_S,
|
|
107
|
+
) -> int:
|
|
108
|
+
"""返回 PID;查不到返回 0(未知),绝不猜。"""
|
|
109
|
+
for attempt in range(max(1, retries)):
|
|
110
|
+
self.refresh(force=attempt > 0)
|
|
111
|
+
row = self._match_local(local_port, remote_addr, remote_port, is_v6)
|
|
112
|
+
if row is not None:
|
|
113
|
+
return row.pid
|
|
114
|
+
if attempt < retries - 1:
|
|
115
|
+
time.sleep(retry_delay_s)
|
|
116
|
+
# 兜底:按 (raddr, rport) 反查
|
|
117
|
+
row = self._match_remote(remote_addr, remote_port)
|
|
118
|
+
if row is not None:
|
|
119
|
+
return row.pid
|
|
120
|
+
return _UNKNOWN_PID
|
|
121
|
+
|
|
122
|
+
def all_rows(self) -> list[TcpRow]:
|
|
123
|
+
self.refresh()
|
|
124
|
+
with self._lock:
|
|
125
|
+
return list(self._rows)
|
|
126
|
+
|
|
127
|
+
def stats(self) -> dict[str, Any]:
|
|
128
|
+
with self._lock:
|
|
129
|
+
return {
|
|
130
|
+
"rows": len(self._rows),
|
|
131
|
+
"refreshes": self.refresh_count,
|
|
132
|
+
"last_error": self.last_error,
|
|
133
|
+
"ttl_s": self.ttl_s,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------ 匹配 -----
|
|
137
|
+
def _match_local(
|
|
138
|
+
self, local_port: int, remote_addr: str | None, remote_port: int | None, is_v6: bool
|
|
139
|
+
) -> TcpRow | None:
|
|
140
|
+
with self._lock:
|
|
141
|
+
row = self._by_port.get((is_v6, local_port)) or self._by_port.get((not is_v6, local_port))
|
|
142
|
+
if row is None:
|
|
143
|
+
return None
|
|
144
|
+
if remote_addr and remote_port:
|
|
145
|
+
if row.remote_addr == remote_addr and row.remote_port == remote_port:
|
|
146
|
+
return row
|
|
147
|
+
# 端口撞车(TIME_WAIT 复用)→ 继续找同端口里 remote 匹配的那条
|
|
148
|
+
with self._lock:
|
|
149
|
+
for candidate in self._rows:
|
|
150
|
+
if (
|
|
151
|
+
candidate.local_port == local_port
|
|
152
|
+
and candidate.remote_addr == remote_addr
|
|
153
|
+
and candidate.remote_port == remote_port
|
|
154
|
+
):
|
|
155
|
+
return candidate
|
|
156
|
+
return None
|
|
157
|
+
return row
|
|
158
|
+
|
|
159
|
+
def _match_remote(self, remote_addr: str | None, remote_port: int | None) -> TcpRow | None:
|
|
160
|
+
if not remote_addr or not remote_port:
|
|
161
|
+
return None
|
|
162
|
+
with self._lock:
|
|
163
|
+
for row in self._rows:
|
|
164
|
+
if row.remote_addr == remote_addr and row.remote_port == remote_port:
|
|
165
|
+
return row
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------- 系统调用 -------
|
|
170
|
+
def _iphlpapi() -> Any:
|
|
171
|
+
import ctypes
|
|
172
|
+
import ctypes.wintypes as wintypes
|
|
173
|
+
|
|
174
|
+
lib = ctypes.WinDLL("iphlpapi", use_last_error=True)
|
|
175
|
+
lib.GetExtendedTcpTable.argtypes = [
|
|
176
|
+
wintypes.LPVOID,
|
|
177
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
178
|
+
wintypes.BOOL,
|
|
179
|
+
wintypes.ULONG,
|
|
180
|
+
wintypes.ULONG,
|
|
181
|
+
wintypes.ULONG,
|
|
182
|
+
]
|
|
183
|
+
lib.GetExtendedTcpTable.restype = wintypes.DWORD
|
|
184
|
+
return lib
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _read_tcp_table(family: int) -> list[TcpRow]:
|
|
188
|
+
"""读 `GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL)`;失败抛 OSError。"""
|
|
189
|
+
import ctypes
|
|
190
|
+
import ctypes.wintypes as wintypes
|
|
191
|
+
|
|
192
|
+
lib = _iphlpapi()
|
|
193
|
+
size = wintypes.DWORD(0)
|
|
194
|
+
rc = lib.GetExtendedTcpTable(None, ctypes.byref(size), False, family, TCP_TABLE_OWNER_PID_ALL, 0)
|
|
195
|
+
if rc not in (0, 122): # 122 = ERROR_INSUFFICIENT_BUFFER
|
|
196
|
+
raise OSError(rc, f"GetExtendedTcpTable(size) 失败 rc={rc}")
|
|
197
|
+
if size.value == 0:
|
|
198
|
+
return []
|
|
199
|
+
|
|
200
|
+
buf = ctypes.create_string_buffer(size.value)
|
|
201
|
+
rc = lib.GetExtendedTcpTable(buf, ctypes.byref(size), False, family, TCP_TABLE_OWNER_PID_ALL, 0)
|
|
202
|
+
if rc != 0:
|
|
203
|
+
raise OSError(rc, f"GetExtendedTcpTable(data) 失败 rc={rc}")
|
|
204
|
+
|
|
205
|
+
raw = bytes(buf.raw[: size.value])
|
|
206
|
+
count = struct.unpack_from("<I", raw, 0)[0]
|
|
207
|
+
return _decode_rows(raw, count, family)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _decode_rows(raw: bytes, count: int, family: int) -> list[TcpRow]:
|
|
211
|
+
"""解析 `MIB_TCPTABLE_OWNER_PID` / `MIB_TCP6TABLE_OWNER_PID` 的裸字节。
|
|
212
|
+
|
|
213
|
+
Windows 结构一律是小端(不要用 `!`)。两个结构的字段顺序不一样,
|
|
214
|
+
这是最容易写错的地方:
|
|
215
|
+
|
|
216
|
+
v4 `MIB_TCPROW_OWNER_PID`(每项 24 字节), `dwState` 在最前:
|
|
217
|
+
0 state / 4 localAddr(4) / 8 localPort / 12 remoteAddr(4) / 16 remotePort / 20 pid
|
|
218
|
+
|
|
219
|
+
v6 `MIB_TCP6ROW_OWNER_PID`(每项 56 字节), `dwState` 在第 7 位,地址在最前:
|
|
220
|
+
0 localAddr(16) / 16 localScopeId / 20 localPort /
|
|
221
|
+
24 remoteAddr(16) / 40 remoteScopeId / 44 remotePort / 48 state / 52 pid
|
|
222
|
+
|
|
223
|
+
> 2026-10-01 真机实测:v6 分支早期照搬了 v4 的"state 在前",于是整行错位
|
|
224
|
+
> 解出来的 `local_port` 恒为 0、`remote_port` 其实读的是 `dwState`(监听的套接字
|
|
225
|
+
> 解出 `state=2`)。后果是所有 v6 连接都查不到 PID,按 `on_unknown_pid=direct`
|
|
226
|
+
> 直接放行 = 真实 IP 泄漏。单测当时和实现犯了同一个错,所以两边都"通过";
|
|
227
|
+
> 是 `pidmap_v6_probe.py` 对着真表才暴露出来。
|
|
228
|
+
|
|
229
|
+
`dwLocalPort`/`dwRemotePort` 的低 16 位是网络序端口(用 `socket.ntohs` 还原)。
|
|
230
|
+
"""
|
|
231
|
+
rows: list[TcpRow] = []
|
|
232
|
+
is_v6 = family == AF_INET6
|
|
233
|
+
row_size = TCP6_ROW_SIZE if is_v6 else TCP_ROW_SIZE
|
|
234
|
+
offset = 4 # 跳过 dwNumEntries
|
|
235
|
+
for _ in range(count):
|
|
236
|
+
if offset + row_size > len(raw):
|
|
237
|
+
break
|
|
238
|
+
if is_v6:
|
|
239
|
+
local_addr = socket.inet_ntop(socket.AF_INET6, raw[offset : offset + 16])
|
|
240
|
+
local_port = _ntoh_port(struct.unpack_from("<I", raw, offset + 20)[0])
|
|
241
|
+
remote_addr = socket.inet_ntop(socket.AF_INET6, raw[offset + 24 : offset + 40])
|
|
242
|
+
remote_port = _ntoh_port(struct.unpack_from("<I", raw, offset + 44)[0])
|
|
243
|
+
state = struct.unpack_from("<I", raw, offset + 48)[0]
|
|
244
|
+
pid = struct.unpack_from("<I", raw, offset + 52)[0]
|
|
245
|
+
else:
|
|
246
|
+
state = struct.unpack_from("<I", raw, offset)[0]
|
|
247
|
+
local_addr = socket.inet_ntoa(raw[offset + 4 : offset + 8])
|
|
248
|
+
local_port = _ntoh_port(struct.unpack_from("<I", raw, offset + 8)[0])
|
|
249
|
+
remote_addr = socket.inet_ntoa(raw[offset + 12 : offset + 16])
|
|
250
|
+
remote_port = _ntoh_port(struct.unpack_from("<I", raw, offset + 16)[0])
|
|
251
|
+
pid = struct.unpack_from("<I", raw, offset + 20)[0]
|
|
252
|
+
rows.append(
|
|
253
|
+
TcpRow(
|
|
254
|
+
state=state,
|
|
255
|
+
local_port=local_port,
|
|
256
|
+
local_addr=local_addr,
|
|
257
|
+
remote_addr=remote_addr,
|
|
258
|
+
remote_port=remote_port,
|
|
259
|
+
pid=pid,
|
|
260
|
+
is_v6=is_v6,
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
offset += row_size
|
|
264
|
+
return rows
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _ntoh_port(dw_port: int) -> int:
|
|
268
|
+
"""`dwLocalPort` 的低 16 位是网络序端口。"""
|
|
269
|
+
return socket.ntohs(dw_port & 0xFFFF)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def is_supported() -> bool:
|
|
273
|
+
import os
|
|
274
|
+
|
|
275
|
+
return os.name == "nt"
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
__all__ = [
|
|
279
|
+
"AF_INET",
|
|
280
|
+
"AF_INET6",
|
|
281
|
+
"DEFAULT_RETRIES",
|
|
282
|
+
"STATE_ESTABLISHED",
|
|
283
|
+
"STATE_SYN_SENT",
|
|
284
|
+
"TCP_TABLE_OWNER_PID_ALL",
|
|
285
|
+
"UDP_TABLE_OWNER_PID",
|
|
286
|
+
"PortPidTable",
|
|
287
|
+
"TcpRow",
|
|
288
|
+
"is_supported",
|
|
289
|
+
]
|