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,46 @@
|
|
|
1
|
+
"""DNS 与 UDP 处理。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .cache import DEFAULT_TTL_S, CacheEntry, DnsCache, on_network_change
|
|
6
|
+
from .modes import (
|
|
7
|
+
MODE_BYPASS,
|
|
8
|
+
MODE_LOCAL,
|
|
9
|
+
MODE_REMOTE,
|
|
10
|
+
DnsPolicy,
|
|
11
|
+
ResolutionPlan,
|
|
12
|
+
build_query,
|
|
13
|
+
parse_answers,
|
|
14
|
+
query_a_record,
|
|
15
|
+
sniff_queries,
|
|
16
|
+
)
|
|
17
|
+
from .udp import (
|
|
18
|
+
MAX_UDP_DATAGRAM,
|
|
19
|
+
UdpForwarder,
|
|
20
|
+
UdpPolicyResolver,
|
|
21
|
+
UdpRelayTransport,
|
|
22
|
+
UdpSession,
|
|
23
|
+
UdpSessionTable,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"DEFAULT_TTL_S",
|
|
28
|
+
"MAX_UDP_DATAGRAM",
|
|
29
|
+
"MODE_BYPASS",
|
|
30
|
+
"MODE_LOCAL",
|
|
31
|
+
"MODE_REMOTE",
|
|
32
|
+
"CacheEntry",
|
|
33
|
+
"DnsCache",
|
|
34
|
+
"DnsPolicy",
|
|
35
|
+
"ResolutionPlan",
|
|
36
|
+
"UdpForwarder",
|
|
37
|
+
"UdpPolicyResolver",
|
|
38
|
+
"UdpRelayTransport",
|
|
39
|
+
"UdpSession",
|
|
40
|
+
"UdpSessionTable",
|
|
41
|
+
"build_query",
|
|
42
|
+
"on_network_change",
|
|
43
|
+
"parse_answers",
|
|
44
|
+
"query_a_record",
|
|
45
|
+
"sniff_queries",
|
|
46
|
+
]
|
proxycore/dns/cache.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""DNS 缓存:IP ↔ 域名映射,带 TTL 且必须标记 inferred。
|
|
2
|
+
|
|
3
|
+
红线:
|
|
4
|
+
* 通过 DNS 嗅探建立的映射只是近似(TTL 内有效,CDN 会误判);
|
|
5
|
+
* 必须带 TTL、必须标记 `inferred=True`、日志里必须能打出 `reason=name-inferred`;
|
|
6
|
+
* 不得用于 BLOCK 决策(`is_trusted()` 只对"确定的域名"返回 True)。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
DEFAULT_TTL_S = 300.0
|
|
17
|
+
MAX_ENTRIES = 20000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class CacheEntry:
|
|
22
|
+
addr: str
|
|
23
|
+
name: str
|
|
24
|
+
expires_at: float
|
|
25
|
+
inferred: bool = True # 嗅探推断出来的 → 近似匹配
|
|
26
|
+
source: str = "dns-sniff"
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> dict[str, Any]:
|
|
29
|
+
return {
|
|
30
|
+
"addr": self.addr,
|
|
31
|
+
"name": self.name,
|
|
32
|
+
"expires_at": self.expires_at,
|
|
33
|
+
"inferred": self.inferred,
|
|
34
|
+
"source": self.source,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DnsCache:
|
|
39
|
+
"""线程安全的 IP→域名表(`dict` + TTL 惰性清理)。"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, *, default_ttl_s: float = DEFAULT_TTL_S, max_entries: int = MAX_ENTRIES,
|
|
42
|
+
clock: Any = time.time) -> None:
|
|
43
|
+
self.default_ttl_s = float(default_ttl_s)
|
|
44
|
+
self.max_entries = max_entries
|
|
45
|
+
self._clock = clock
|
|
46
|
+
self._lock = threading.RLock()
|
|
47
|
+
self._by_ip: dict[str, CacheEntry] = {}
|
|
48
|
+
self._flush_count = 0
|
|
49
|
+
# 每次内容变化 +1。`GuardSet.inferred_names` 需要跟着缓存走,但
|
|
50
|
+
# `snapshot()` 是 O(条目数)(上限 20000),不能在决策热路径上每秒重算一遍;
|
|
51
|
+
# 调用方比对这个数就知道"要不要刷新"。
|
|
52
|
+
self._revision = 0
|
|
53
|
+
|
|
54
|
+
# ------------------------------------------------------------ 写入 -----
|
|
55
|
+
def put(
|
|
56
|
+
self,
|
|
57
|
+
addr: str,
|
|
58
|
+
name: str,
|
|
59
|
+
*,
|
|
60
|
+
ttl_s: float | None = None,
|
|
61
|
+
inferred: bool = True,
|
|
62
|
+
source: str = "dns-sniff",
|
|
63
|
+
) -> CacheEntry:
|
|
64
|
+
ttl = self.default_ttl_s if ttl_s is None else max(1.0, float(ttl_s))
|
|
65
|
+
entry = CacheEntry(
|
|
66
|
+
addr=addr,
|
|
67
|
+
name=name,
|
|
68
|
+
expires_at=self._clock() + ttl,
|
|
69
|
+
inferred=inferred,
|
|
70
|
+
source=source,
|
|
71
|
+
)
|
|
72
|
+
with self._lock:
|
|
73
|
+
self._by_ip[addr] = entry
|
|
74
|
+
self._revision += 1
|
|
75
|
+
if len(self._by_ip) > self.max_entries:
|
|
76
|
+
self._evict_locked()
|
|
77
|
+
return entry
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------ 读取 -----
|
|
80
|
+
def get(self, addr: str) -> str | None:
|
|
81
|
+
"""取域名(过期即删)。"""
|
|
82
|
+
with self._lock:
|
|
83
|
+
entry = self._by_ip.get(addr)
|
|
84
|
+
if entry is None:
|
|
85
|
+
return None
|
|
86
|
+
if entry.expires_at <= self._clock():
|
|
87
|
+
del self._by_ip[addr]
|
|
88
|
+
self._revision += 1 # 让 inferred_names 的持有者知道内容变了
|
|
89
|
+
return None
|
|
90
|
+
return entry.name
|
|
91
|
+
|
|
92
|
+
def entry(self, addr: str) -> CacheEntry | None:
|
|
93
|
+
with self._lock:
|
|
94
|
+
entry = self._by_ip.get(addr)
|
|
95
|
+
if entry is None or entry.expires_at <= self._clock():
|
|
96
|
+
if self._by_ip.pop(addr, None) is not None:
|
|
97
|
+
self._revision += 1
|
|
98
|
+
return None
|
|
99
|
+
return entry
|
|
100
|
+
|
|
101
|
+
def is_inferred(self, addr: str) -> bool:
|
|
102
|
+
entry = self.entry(addr)
|
|
103
|
+
return bool(entry and entry.inferred)
|
|
104
|
+
|
|
105
|
+
def is_trusted(self, addr: str) -> bool:
|
|
106
|
+
"""只有"确定的域名"才可信;推断出来的映射不可用于 BLOCK 决策。"""
|
|
107
|
+
entry = self.entry(addr)
|
|
108
|
+
return bool(entry and not entry.inferred)
|
|
109
|
+
|
|
110
|
+
def resolve(self, addr: str) -> str | None:
|
|
111
|
+
"""`GuardSet.name_resolver` 的签名。"""
|
|
112
|
+
return self.get(addr)
|
|
113
|
+
|
|
114
|
+
def __len__(self) -> int:
|
|
115
|
+
with self._lock:
|
|
116
|
+
self._purge_locked()
|
|
117
|
+
return len(self._by_ip)
|
|
118
|
+
|
|
119
|
+
def snapshot(self) -> list[dict[str, Any]]:
|
|
120
|
+
with self._lock:
|
|
121
|
+
self._purge_locked()
|
|
122
|
+
return [e.to_dict() for e in self._by_ip.values()]
|
|
123
|
+
|
|
124
|
+
def stats(self) -> dict[str, Any]:
|
|
125
|
+
with self._lock:
|
|
126
|
+
self._purge_locked()
|
|
127
|
+
inferred = sum(1 for e in self._by_ip.values() if e.inferred)
|
|
128
|
+
return {
|
|
129
|
+
"entries": len(self._by_ip),
|
|
130
|
+
"inferred": inferred,
|
|
131
|
+
"flushes": self._flush_count,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# ------------------------------------------------------------ 清理 -----
|
|
135
|
+
def clear_inferred(self) -> int:
|
|
136
|
+
"""网络切换时清掉推断映射。"""
|
|
137
|
+
with self._lock:
|
|
138
|
+
victims = [addr for addr, e in self._by_ip.items() if e.inferred]
|
|
139
|
+
for addr in victims:
|
|
140
|
+
del self._by_ip[addr]
|
|
141
|
+
self._flush_count += 1
|
|
142
|
+
self._revision += 1
|
|
143
|
+
return len(victims)
|
|
144
|
+
|
|
145
|
+
def clear(self) -> int:
|
|
146
|
+
with self._lock:
|
|
147
|
+
count = len(self._by_ip)
|
|
148
|
+
self._by_ip.clear()
|
|
149
|
+
self._flush_count += 1
|
|
150
|
+
self._revision += 1
|
|
151
|
+
return count
|
|
152
|
+
|
|
153
|
+
# -------------------------------------------------------- 变更通知 -----
|
|
154
|
+
@property
|
|
155
|
+
def revision(self) -> int:
|
|
156
|
+
"""内容版本号;调用方据此判断"要不要重新读一遍缓存"。"""
|
|
157
|
+
with self._lock:
|
|
158
|
+
return self._revision
|
|
159
|
+
|
|
160
|
+
def inferred_names(self) -> set[str]:
|
|
161
|
+
"""当前未过期的推断域名集合(`GuardSet.inferred_names` 的来源)。"""
|
|
162
|
+
with self._lock:
|
|
163
|
+
self._purge_locked()
|
|
164
|
+
return {e.name for e in self._by_ip.values() if e.inferred}
|
|
165
|
+
|
|
166
|
+
def _purge_locked(self) -> int:
|
|
167
|
+
now = self._clock()
|
|
168
|
+
victims = [addr for addr, e in self._by_ip.items() if e.expires_at <= now]
|
|
169
|
+
for addr in victims:
|
|
170
|
+
del self._by_ip[addr]
|
|
171
|
+
if victims:
|
|
172
|
+
self._revision += 1
|
|
173
|
+
return len(victims)
|
|
174
|
+
|
|
175
|
+
def _evict_locked(self) -> None:
|
|
176
|
+
# 先清过期,再按即将到期顺序淘汰
|
|
177
|
+
self._purge_locked()
|
|
178
|
+
overflow = len(self._by_ip) - self.max_entries
|
|
179
|
+
if overflow <= 0:
|
|
180
|
+
return
|
|
181
|
+
for addr, _ in sorted(self._by_ip.items(), key=lambda kv: kv[1].expires_at)[:overflow]:
|
|
182
|
+
del self._by_ip[addr]
|
|
183
|
+
self._revision += 1
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def on_network_change(cache: DnsCache, *, flush_system: bool = False) -> dict[str, Any]:
|
|
187
|
+
"""网络接口变化时的处理(等效于 `ipconfig /flushdns`)。"""
|
|
188
|
+
import subprocess
|
|
189
|
+
|
|
190
|
+
removed = cache.clear_inferred()
|
|
191
|
+
result: dict[str, Any] = {"cleared_inferred": removed, "system_flush": False}
|
|
192
|
+
if flush_system:
|
|
193
|
+
from ..proc import IS_WINDOWS
|
|
194
|
+
|
|
195
|
+
cmd = ["ipconfig", "/flushdns"] if IS_WINDOWS else ["resolvectl", "flush-caches"]
|
|
196
|
+
try:
|
|
197
|
+
subprocess.run(cmd, capture_output=True, check=False, timeout=5)
|
|
198
|
+
result["system_flush"] = True
|
|
199
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
200
|
+
result["system_flush_error"] = str(exc)
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
__all__ = [
|
|
205
|
+
"DEFAULT_TTL_S",
|
|
206
|
+
"CacheEntry",
|
|
207
|
+
"DnsCache",
|
|
208
|
+
"on_network_change",
|
|
209
|
+
]
|