Tor部署Discuz论坛

前言

前面两篇文章讲了如何部署一个tor网站以及如何生成特定前缀的onion域名,今天这篇文章主要讲的是实操,如何部署一个Tor的论坛,换句话说,如何将在暗网部署一个论坛,有了前面两篇文章的了解和基础,这篇文章做起来就比较容易!
这里我用的是国内比较出名的开源论坛,Discuz!

仅限学习交流,不得用于任何违法犯罪用途,若用于违法犯罪用途,因此产生的法律后果需要你自行承担,博主不承担任何法律责任!

仅限学习交流,不得用于任何违法犯罪用途,若用于违法犯罪用途,因此产生的法律后果需要你自行承担,博主不承担任何法律责任!

仅限学习交流,不得用于任何违法犯罪用途,若用于违法犯罪用途,因此产生的法律后果需要你自行承担,博主不承担任何法律责任!

一、环境准备

1、Ubuntu 24.04.3 LTS
2、Nginx
3、php8.3
4、Mariadb 10.11.13
5、Redis8.2.1 https://github.com/redis/redis/releases
6、Tor https://www.torproject.org/
7、Discuz! X5.0 https://gitee.com/Discuz/DiscuzX/tree/MitFrame/

二、安装和配置Nginx

1
2
3
4
5
6
apt update -y
apt upgrade -y
apt install nginx
mkdir -p /www/tor
mkdir -p /etc/nginx/logs/
vi /etc/nginx/conf.d/dz.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
listen unix:/www/tor/dz/dz.sock;
server_name _;
access_log /etc/nginx/logs/dzonionaccess.log;
error_log /etc/nginx/logs/dzonionerror.log;

location / {
root /www/DiscuzX/upload/;
index index.php index.html index.htm;
}

location ~ \.php$ {
root /www/DiscuzX/upload/;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

三、安装PHP8.3

1
sudo apt install php8.3 php8.3-fpm php8.3-cli php8.3-curl php8.3-mysql php8.3-gd php8.3-xml php8.3-mbstring php8.3-zip php8.3-opcache php8.3-redis

四、安装并初始化mariadb

1
2
apt install mariadb
mysql_secure_installation

按照提示进行配置即可,初次安装没有密码,需要根据提示设定一个密码。

五、编译安装并配置Redis

1
2
3
4
5
6
wget https://github.com/redis/redis/archive/refs/tags/8.2.1.tar.gz
tar -zxvf 8.2.1.tar.gz
cd redis-8.2.1
make && make install PREFIX=/usr/local/redis
cd /usr/local/redis
vi redis.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
bind 127.0.0.1 -::1
protected-mode yes
port 6379
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize no
pidfile /var/run/redis_6379.pid
loglevel notice
logfile ""
databases 16
always-show-logo no
set-proc-title yes
proc-title-template "{title} {listen-addr} {server-mode}"
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
rdb-del-sync-files no
dir ./
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync yes
repl-diskless-sync-delay 5
repl-diskless-sync-max-replicas 0
repl-diskless-load disabled
repl-disable-tcp-nodelay no
replica-priority 100
acllog-max-len 128
requirepass 123456789@abc.com
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
lazyfree-lazy-user-del no
lazyfree-lazy-user-flush no
oom-score-adj no
oom-score-adj-values 0 200 800
disable-thp yes
appendonly no
appendfilename "appendonly.aof"
appenddirname "appendonlydir"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
aof-timestamp-enabled no

slowlog-log-slower-than 10000
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
hash-max-listpack-entries 512
hash-max-listpack-value 64
list-max-listpack-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-listpack-entries 128
zset-max-listpack-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
jemalloc-bg-thread yes

六、安装并配置Tor

如果你介意安全问题,那你就看下这两个文档来配置下ubuntu的Tor源,然后apt安装
https://community.torproject.org/onion-services/setup/install/

https://support.torproject.org/apt/tor-deb-repo/

如果你不介意安全问题的话,就直接apt安装

1
apt install tor

1
vi /etc/tor/torrc

新增这两行

1
2
HiddenServiceDir /www/tor/dz/
HiddenServicePort 80 unix:/www/tor/dz/dz.sock
1
vi /usr/lib/systemd/system/tor@default.service

在其他ReadWriteDirectories下加一条

1
ReadWriteDirectories=-/www/tor
1
vi /etc/apparmor.d/system_tor

然后新增

1
2
3
owner /www/tor/** rwk,
owner /www/tor/ r,
/www/tor/** r,

最终是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# vim:syntax=apparmor
#include <tunables/global>

profile system_tor flags=(attach_disconnected) {
#include <abstractions/tor>

owner /var/lib/tor/** rwk,
owner /www/tor/** rwk,
owner /www/tor/ r,
owner /var/lib/tor/ r,
owner /var/log/tor/* w,

# During startup, tor (as root) tries to open various things such as
# directories via check_private_dir(). Let it.
/var/lib/tor/** r,
/www/tor/** r,
/{,var/}run/tor/ r,
/{,var/}run/tor/control w,
/{,var/}run/tor/socks w,
/{,var/}run/tor/tor.pid w,
/{,var/}run/tor/control.authcookie w,
/{,var/}run/tor/control.authcookie.tmp rw,
/{,var/}run/systemd/notify w,

# Site-specific additions and overrides. See local/README for details.
#include <local/system_tor>
}

七、git项目并修改php代码

1
2
3
4
cd /www
git clone https://gitee.com/Discuz/DiscuzX.git
cd DiscuzX/upload/install
vi index.php

找到这一段

1
2
3
4
5
6
7
if($uid) {
$db->query("REPLACE INTO {$tablepre}common_member_count SET uid='$uid';");
$db->query("REPLACE INTO {$tablepre}common_member_status SET uid='$uid';");
$db->query("REPLACE INTO {$tablepre}common_member_field_forum SET uid='$uid';");
$db->query("REPLACE INTO {$tablepre}common_member_field_home SET uid='$uid';");
$db->query("REPLACE INTO {$tablepre}common_member_profile SET uid='$uid';");
}

修改成

1
2
3
4
5
6
7
if($uid) {
$db->query("REPLACE INTO {$tablepre}common_member_count SET uid='$uid';");
$db->query("REPLACE INTO {$tablepre}common_member_status SET uid='$uid';");
$db->query("REPLACE INTO {$tablepre}common_member_field_forum SET uid='$uid';");
$db->query("REPLACE INTO {$tablepre}common_member_field_home SET uid='$uid';");
$db->query("REPLACE INTO {$tablepre}common_member_profile SET uid='$uid', fields='{}';"); // 添加 fields='{}'
}

然后修改class_ip.php

1
vi DiscuzX/upload/source/class/class_ip.php

原来的class_ip.php内容是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
<?php

/**
* [Discuz!] (C)2001-2099 Discuz! Team
* This is NOT a freeware, use is subject to license terms
* https://license.discuz.vip
*/

if(!defined('IN_DISCUZ')) {
exit('Access Denied');
}

class ip {

function __construct() {
}

/*
* 将IPv6地址外面加方括号,用于显示
*/
public static function to_display($ip) {
if(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return '['.$ip.']';
}
return $ip;
}

/*
* 将各种显示格式的IPv6地址处理回标准IPv6格式
* [::1] -> ::1
* [::1]/16 -> ::1/16
*/
public static function to_ip($ip) {
if(strlen($ip) == 0) return $ip;
if(preg_match('/(.*?)\[((.*?:)+.*)\](.*)/', $ip, $m)) { // [xx:xx:xx]格式
if(filter_var($m[2], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ip = $m[1].$m[2].$m[4];
}
}
return $ip;
}

/*
* 验证IP是否合法,支持v4和v6
*/
public static function validate_ip($ip) {
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
}

/*
* 验证是否是合法的CIDR:
* - 包含 /
* - / 后面大于0
* - / 前面是合法的IP
* 返回值:
* - TRUE,表示是合法的CIDR,$new_str为处理过的CIDR(IP部分调用了to_ip)
* - FALSE, 不是合法的CIDR
*/
public static function validate_cidr($str, &$new_str) {
if(str_contains($str, '/')) {
[$newip, $mask] = explode('/', $str);
if($mask <= 0) {
return FALSE;
}
$newmask = intval($mask);
$newip = self::to_ip($newip);
if(!self::validate_ip($newip)) {
return FALSE;
}
if($newmask > 128 || ($newmask > 32 && !str_contains($newip, ':'))) {
return FALSE;
}
$new_str = $newip.'/'.$mask;
return TRUE;
}
return FALSE;
}

/*
* 给一个ipv4或v6的cidr,计算最小IP和最大IP
* 如果输入的是一个IP,那最大最小IP都等于其自身
* $as_hex = true
* 返回值为 二进制表达的字符串格式
* $as_hex = false
* 返回值可用inet_ntop轮换为IP字符串表达式
*/
public static function calc_cidr_range($str, $as_hex = false) {
if(self::validate_cidr($str, $str)) {
[$ip, $prefix] = explode('/', $str);
} elseif(filter_var($str, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$ip = $str;
$prefix = 32;
} elseif(filter_var($str, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ip = $str;
$prefix = 128;
} else {
return FALSE;
}

$ip_bytes = unpack('C*', inet_pton($ip));
$total_bytes = count($ip_bytes);
$num_diff_bits = 8 * $total_bytes - $prefix;
if($num_diff_bits >= 0) {
$num_same_bytes = $prefix >> 3;
$same_bytes = array_slice($ip_bytes, 0, $num_same_bytes);
$diff_bytes_start = ($total_bytes === $num_same_bytes) ? [] : array_fill(0, $total_bytes - $num_same_bytes, 0);
$diff_bytes_end = ($total_bytes === $num_same_bytes) ? [] : array_fill(0, $total_bytes - $num_same_bytes, 255);
$start_same_bits = $prefix % 8;
if($start_same_bits !== 0) {
$vary_byte = $ip_bytes[$num_same_bytes];
$diff_bytes_start[0] = $vary_byte & bindec(str_pad(str_repeat('1', $start_same_bits), 8, '0', STR_PAD_RIGHT));
$diff_bytes_end[0] = $diff_bytes_start[0] + bindec(str_repeat('1', 8 - $start_same_bits));
}

$start_array = array_merge($same_bytes, $diff_bytes_start);
$end_array = array_merge($same_bytes, $diff_bytes_end);
if($as_hex) {
if($total_bytes < 16) {
$start_array = array_merge(array_fill(0, 16 - $total_bytes, 0), $start_array);
$end_array = array_merge(array_fill(0, 16 - $total_bytes, 0), $end_array);
}
$start = unpack('H*hex', join(array_map('chr', $start_array)))['hex'];
$end = unpack('H*hex', join(array_map('chr', $end_array)))['hex'];
return [$start, $end];
} else {
$start = call_user_func_array('pack', array_merge(['C*'], $start_array));
$end = call_user_func_array('pack', array_merge(['C*'], $end_array));
return [$start, $end];
}
}

return FALSE;
}

/*
* 将一个IP地址转为16进制表达的字符串
*/
public static function ip_to_hex_str($ip) {
if(!self::validate_ip($ip)) {
return false;
}
$ip_bytes = unpack('C*', inet_pton($ip));
$total_bytes = count($ip_bytes);
if($total_bytes < 16) {
$ip_bytes = array_merge(array_fill(0, 16 - $total_bytes, 0), $ip_bytes);
}
return unpack('H*hex', join(array_map('chr', $ip_bytes)))['hex'];
}

/*
* 以下三个函数,检查$requestIp是否在$ip给出的cidr范围内
*/

public static function check_ip($requestIp, $ips) {
if(!self::validate_ip($requestIp)) {
return false;
}
if(!\is_array($ips)) {
$ips = [$ips];
}
$method = substr_count($requestIp, ':') > 1 ? 'check_ip6' : 'check_ip4';
foreach($ips as $ip) {
if(self::$method($requestIp, $ip)) {
return true;
}
}
return false;
}

public static function check_ip6($requestIp, $ip) {
if(str_contains($ip, '/')) {
[$address, $netmask] = explode('/', $ip, 2);
if('0' === $netmask) {
return (bool)unpack('n*', @inet_pton($address));
}
if($netmask < 1 || $netmask > 128) {
return false;
}
} else {
$address = $ip;
$netmask = 128;
}
$bytesAddr = unpack('n*', @inet_pton($address));
$bytesTest = unpack('n*', @inet_pton($requestIp));
if(!$bytesAddr || !$bytesTest) {
return false;
}
for($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
$left = $netmask - 16 * ($i - 1);
$left = ($left <= 16) ? $left : 16;
$mask = ~(0xffff >> $left) & 0xffff;
if(($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
return false;
}
}
return true;
}

public static function check_ip4($requestIp, $ip) {
if(str_contains($ip, '/')) {
[$address, $netmask] = explode('/', $ip, 2);
if('0' === $netmask) {
return false;
}
if($netmask < 0 || $netmask > 32) {
return false;
}
} else {
$address = $ip;
$netmask = 32;
}
if(false === ip2long($address)) {
return false;
}
return 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
}

/*
* 将IP转为位置,支持传入CIDR
*/
public static function convert($ip) {
$return = '';
require childfile('ip', 'global/core');
return $return;
}

public static function checkaccess($ip, $accesslist) {
return preg_match('/^('.str_replace(["\r\n", ' '], ['|', ''], preg_quote($accesslist, '/')).')/', $ip);
}

public static function checkbanned($ip) {
global $_G;

if(array_key_exists('security', $_G['config']) && array_key_exists('useipban', $_G['config']['security']) && $_G['config']['security']['useipban'] == 0) {
return false;
}

if($_G['setting']['ipaccess'] && !self::checkaccess($ip, $_G['setting']['ipaccess'])) {
return true;
}

return table_common_banned::t()->check_banned(TIMESTAMP, $ip);
}

}

我们改成这样,来支持onion域名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
<?php

/**
* [Discuz!] (C)2001-2099 Discuz! Team
* This is NOT a freeware, use is subject to license terms
* https://license.discuz.vip
*
* Modified to support Tor onion addresses
*/

if(!defined('IN_DISCUZ')) {
exit('Access Denied');
}

class ip {

function __construct() {
}

/*
* 验证是否是有效的 onion 地址
* v2 onion: 16字符的base32编码 + .onion
* v3 onion: 56字符的base32编码 + .onion
*/
public static function validate_onion($address) {
if(!str_ends_with($address, '.onion')) {
return false;
}

$onion_part = substr($address, 0, -6); // 移除 .onion 后缀

// 检查是否只包含有效的 base32 字符 (a-z, 2-7)
if(!preg_match('/^[a-z2-7]+$/', $onion_part)) {
return false;
}

// v2 onion: 16字符
// v3 onion: 56字符
$len = strlen($onion_part);
return ($len == 16 || $len == 56);
}

/*
* 将IPv6地址外面加方括号,用于显示
* 扩展支持 onion 地址
*/
public static function to_display($ip) {
if(self::validate_onion($ip)) {
return $ip; // onion 地址不需要方括号
}
if(filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
return '['.$ip.']';
}
return $ip;
}

/*
* 将各种显示格式的IPv6地址处理回标准IPv6格式
* [::1] -> ::1
* [::1]/16 -> ::1/16
* 对 onion 地址保持原样
*/
public static function to_ip($ip) {
if(strlen($ip) == 0) return $ip;

// 如果是 onion 地址,直接返回
if(self::validate_onion($ip)) {
return $ip;
}

if(preg_match('/(.*?)\[((.*?:)+.*)\](.*)/', $ip, $m)) { // [xx:xx:xx]格式
if(filter_var($m[2], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ip = $m[1].$m[2].$m[4];
}
}
return $ip;
}

/*
* 验证IP是否合法,支持v4、v6和onion
*/
public static function validate_ip($ip) {
// 先检查是否是 onion 地址
if(self::validate_onion($ip)) {
return true;
}
// 再检查传统 IP
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
}

/*
* 验证是否是合法的CIDR:
* - 包含 /
* - / 后面大于0
* - / 前面是合法的IP
* 注意:onion 地址不支持 CIDR 表示法
* 返回值:
* - TRUE,表示是合法的CIDR,$new_str为处理过的CIDR(IP部分调用了to_ip)
* - FALSE, 不是合法的CIDR
*/
public static function validate_cidr($str, &$new_str) {
if(str_contains($str, '/')) {
[$newip, $mask] = explode('/', $str);
if($mask <= 0) {
return FALSE;
}
$newmask = intval($mask);
$newip = self::to_ip($newip);

// onion 地址不支持 CIDR
if(self::validate_onion($newip)) {
return FALSE;
}

if(!self::validate_ip($newip)) {
return FALSE;
}
if($newmask > 128 || ($newmask > 32 && !str_contains($newip, ':'))) {
return FALSE;
}
$new_str = $newip.'/'.$mask;
return TRUE;
}
return FALSE;
}

/*
* 给一个ipv4或v6的cidr,计算最小IP和最大IP
* 如果输入的是一个IP,那最大最小IP都等于其自身
* onion 地址不支持范围计算,返回自身
* $as_hex = true
* 返回值为 二进制表达的字符串格式
* $as_hex = false
* 返回值可用inet_ntop轮换为IP字符串表达式
*/
public static function calc_cidr_range($str, $as_hex = false) {
// onion 地址特殊处理
if(self::validate_onion($str)) {
if($as_hex) {
// 为 onion 地址生成一个唯一的十六进制表示
$hex = md5($str);
return [$hex, $hex];
} else {
return [$str, $str];
}
}

if(self::validate_cidr($str, $str)) {
[$ip, $prefix] = explode('/', $str);
} elseif(filter_var($str, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$ip = $str;
$prefix = 32;
} elseif(filter_var($str, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$ip = $str;
$prefix = 128;
} else {
return FALSE;
}

$ip_bytes = unpack('C*', inet_pton($ip));
$total_bytes = count($ip_bytes);
$num_diff_bits = 8 * $total_bytes - $prefix;
if($num_diff_bits >= 0) {
$num_same_bytes = $prefix >> 3;
$same_bytes = array_slice($ip_bytes, 0, $num_same_bytes);
$diff_bytes_start = ($total_bytes === $num_same_bytes) ? [] : array_fill(0, $total_bytes - $num_same_bytes, 0);
$diff_bytes_end = ($total_bytes === $num_same_bytes) ? [] : array_fill(0, $total_bytes - $num_same_bytes, 255);
$start_same_bits = $prefix % 8;
if($start_same_bits !== 0) {
$vary_byte = $ip_bytes[$num_same_bytes];
$diff_bytes_start[0] = $vary_byte & bindec(str_pad(str_repeat('1', $start_same_bits), 8, '0', STR_PAD_RIGHT));
$diff_bytes_end[0] = $diff_bytes_start[0] + bindec(str_repeat('1', 8 - $start_same_bits));
}

$start_array = array_merge($same_bytes, $diff_bytes_start);
$end_array = array_merge($same_bytes, $diff_bytes_end);
if($as_hex) {
if($total_bytes < 16) {
$start_array = array_merge(array_fill(0, 16 - $total_bytes, 0), $start_array);
$end_array = array_merge(array_fill(0, 16 - $total_bytes, 0), $end_array);
}
$start = unpack('H*hex', join(array_map('chr', $start_array)))['hex'];
$end = unpack('H*hex', join(array_map('chr', $end_array)))['hex'];
return [$start, $end];
} else {
$start = call_user_func_array('pack', array_merge(['C*'], $start_array));
$end = call_user_func_array('pack', array_merge(['C*'], $end_array));
return [$start, $end];
}
}

return FALSE;
}

/*
* 将一个IP地址转为16进制表达的字符串
* 对于 onion 地址,使用 MD5 哈希作为十六进制表示
*/
public static function ip_to_hex_str($ip) {
// onion 地址特殊处理
if(self::validate_onion($ip)) {
return md5($ip);
}

if(!self::validate_ip($ip)) {
return false;
}
$ip_bytes = unpack('C*', inet_pton($ip));
$total_bytes = count($ip_bytes);
if($total_bytes < 16) {
$ip_bytes = array_merge(array_fill(0, 16 - $total_bytes, 0), $ip_bytes);
}
return unpack('H*hex', join(array_map('chr', $ip_bytes)))['hex'];
}

/*
* 以下三个函数,检查$requestIp是否在$ip给出的cidr范围内
* 对于 onion 地址,只进行精确匹配
*/

public static function check_ip($requestIp, $ips) {
if(!self::validate_ip($requestIp)) {
return false;
}
if(!\is_array($ips)) {
$ips = [$ips];
}

// 如果请求的是 onion 地址,只进行精确匹配
if(self::validate_onion($requestIp)) {
foreach($ips as $ip) {
if($requestIp === $ip) {
return true;
}
}
return false;
}

$method = substr_count($requestIp, ':') > 1 ? 'check_ip6' : 'check_ip4';
foreach($ips as $ip) {
// 如果规则是 onion 地址,进行精确匹配
if(self::validate_onion($ip)) {
if($requestIp === $ip) {
return true;
}
continue;
}
if(self::$method($requestIp, $ip)) {
return true;
}
}
return false;
}

public static function check_ip6($requestIp, $ip) {
// 如果任一个是 onion 地址,进行精确匹配
if(self::validate_onion($requestIp) || self::validate_onion($ip)) {
return $requestIp === $ip;
}

if(str_contains($ip, '/')) {
[$address, $netmask] = explode('/', $ip, 2);
if('0' === $netmask) {
return (bool)unpack('n*', @inet_pton($address));
}
if($netmask < 1 || $netmask > 128) {
return false;
}
} else {
$address = $ip;
$netmask = 128;
}
$bytesAddr = unpack('n*', @inet_pton($address));
$bytesTest = unpack('n*', @inet_pton($requestIp));
if(!$bytesAddr || !$bytesTest) {
return false;
}
for($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) {
$left = $netmask - 16 * ($i - 1);
$left = ($left <= 16) ? $left : 16;
$mask = ~(0xffff >> $left) & 0xffff;
if(($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) {
return false;
}
}
return true;
}

public static function check_ip4($requestIp, $ip) {
// 如果任一个是 onion 地址,进行精确匹配
if(self::validate_onion($requestIp) || self::validate_onion($ip)) {
return $requestIp === $ip;
}

if(str_contains($ip, '/')) {
[$address, $netmask] = explode('/', $ip, 2);
if('0' === $netmask) {
return false;
}
if($netmask < 0 || $netmask > 32) {
return false;
}
} else {
$address = $ip;
$netmask = 32;
}
if(false === ip2long($address)) {
return false;
}
return 0 === substr_compare(sprintf('%032b', ip2long($requestIp)), sprintf('%032b', ip2long($address)), 0, $netmask);
}

/*
* 将IP转为位置,支持传入CIDR
* onion 地址返回 "Tor Network"
*/
public static function convert($ip) {
$return = '';

// 如果是 onion 地址,返回 Tor 网络标识
if(self::validate_onion($ip)) {
return 'Tor Network';
}

require childfile('ip', 'global/core');
return $return;
}

public static function checkaccess($ip, $accesslist) {
// 对于 onion 地址,使用精确匹配而不是正则匹配
if(self::validate_onion($ip)) {
$allowed_ips = preg_split('/[\r\n\s]+/', $accesslist, -1, PREG_SPLIT_NO_EMPTY);
return in_array($ip, $allowed_ips);
}

return preg_match('/^('.str_replace(["\r\n", ' '], ['|', ''], preg_quote($accesslist, '/')).')/', $ip);
}

public static function checkbanned($ip) {
global $_G;

if(array_key_exists('security', $_G['config']) && array_key_exists('useipban', $_G['config']['security']) && $_G['config']['security']['useipban'] == 0) {
return false;
}

if($_G['setting']['ipaccess'] && !self::checkaccess($ip, $_G['setting']['ipaccess'])) {
return true;
}

return table_common_banned::t()->check_banned(TIMESTAMP, $ip);
}

}

主要修改内容:
1. 新增 onion 地址验证函数
phppublic static function validate_onion($address)

验证 v2 onion(16字符)和 v3 onion(56字符)地址
检查 base32 编码格式和 .onion 后缀

2. 扩展现有函数以支持 onion 地址
validate_ip() : 现在也接受 onion 地址作为有效地址
to_display() : onion 地址不需要方括号
to_ip() : onion 地址保持原样
calc_cidr_range() : onion 地址返回自身(不支持范围)
ip_to_hex_str() : onion 地址使用 MD5 哈希表示
3. 修改 IP 检查逻辑
check_ip(): onion 地址只进行精确匹配
check_ip4() check_ip6() : 遇到 onion 地址时进行精确比较
checkaccess() : onion 地址使用精确匹配而不是正则匹配
4. 地理位置支持
convert() : onion 地址返回 "Tor Network" 标识
使用注意事项:

CIDR 不支持: onion 地址不支持 CIDR 表示法,只能精确匹配
访问控制: 在 IP 访问列表中,onion 地址需要完整写出
日志记录: onion 地址会被正常记录在访问日志中
兼容性: 所有现有的 IPv4/IPv6 功能保持不变
这样修改后,Discuz! 就可以正确处理来自 Tor 网络的 onion 地址访问了。

接着修改 table_common_banned.php

vi /www/DiscuzX/upload/source/class/table/table_common_banned.php

原文是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?php
/**
* [Discuz!] (C)2001-2099 Discuz! Team
* This is NOT a freeware, use is subject to license terms
* https://license.discuz.vip
/
if(!defined('IN_DISCUZ')) {
exit('Access Denied');
}
class table_common_banned extends discuz_table {
/
* 在memory启用的时候,存储于common_banned_index的SortedSet中
* member = ip的16进制表达, score = 1 表示封禁,score = 0 表示不封禁
*/
public static function t() {
static $_instance;
if(!isset($_instance)) {
$_instance = new self();
}
return $_instance;
}
public function construct() {
$this->_table = 'common_banned';
$this->_pk = 'id';
$this->_pre_cache_key = 'commonbanned';
$this->_cache_ttl = 600;
parent::construct();
$this->_allowmem = $this->_allowmem && C::memory()->gotsortedset;
}
public function fetch_by_ip($ip) {
return DB::fetch_first('SELECT * FROM %t WHERE ip=%s', [$this->_table, $ip]);
}
public function fetch_all_order_dateline() {
return DB::fetch_all('SELECT * FROM %t ORDER BY dateline DESC', [$this->_table]);
}
public function fetch_all($ids = [], $force_from_db = false) {
// Todo: $ids = array() 需要在取消兼容层后删除
if(defined('DISCUZ_DEPRECATED')) {
throw new Exception('NotImplementedException');
return parent::fetch_all($ids, $force_from_db);
} else {
return $this->fetch_all_banned();
}
}
public function fetch_all_banned() {
return DB::fetch_all('SELECT * FROM %t', [$this->_table]);
}
public function delete_by_id($ids, $adminid, $adminname) {
$ids = array_map('intval', (array)$ids);
if($ids) {
if($this->_allowmem) memory('rm', 'index', $this->_pre_cache_key);
return DB::query('DELETE FROM %t WHERE id IN(%n) AND (1=%d OR admin=%s)', [$this->_table, $ids, $adminid, $adminname]);
}
return 0;
}
public function update_expiration_by_id($id, $expiration, $isadmin, $admin) {
if($this->_allowmem) memory('rm', 'index', $this->_pre_cache_key);
return DB::query('UPDATE %t SET expiration=%d WHERE id=%d AND (1=%d OR admin=%s)', [$this->_table, $expiration, $id, $isadmin, $admin]);
}
public function insert($data, $return_insert_id = false, $replace = false, $silent = false) {
$cmd = $replace ? 'REPLACE INTO' : 'INSERT INTO';
if(!str_starts_with($data['lowerip'], '0x')) $data['lowerip'] = '0x'.$data['lowerip'];
if(!str_starts_with($data['upperip'], '0x')) $data['upperip'] = '0x'.$data['upperip'];
if($this->_allowmem) memory('rm', 'index', $this->_pre_cache_key);
return DB::query(
$cmd.' %t SET ip=%s, lowerip=%i, upperip=%i, admin=%s, dateline=%d, expiration=%d',
[$this->_table, $data['ip'], $data['lowerip'], $data['upperip'], $data['admin'], $data['dateline'], $data['expiration']],
$silent, !$return_insert_id
);
}
public function check_banned($time_to_check, $ip) {
$iphex = ip::ip_to_hex_str($ip);
$banned = true;
if($this->_allowmem) $banned = memory('zscore', 'index', $iphex, 0, $this->_pre_cache_key);
if($banned === false || !$this->_allowmem) { // 如果memory中没有值,或不使用memory,都走数据库
$iphex_val = '0x'.$iphex;
$ret = DB::result_first(
'SELECT id from %t WHERE expiration > %d AND lowerip <= %i AND upperip >= %i',
[$this->_table, $time_to_check, $iphex_val, $iphex_val]
);
if($ret) {
if($this->_allowmem) memory('zadd', 'index', $iphex, 1, $this->_pre_cache_key);
return true;
}
if($this->_allowmem) memory('zadd', 'index', $iphex, 0, $this->_pre_cache_key);
return false;
}
// _allowmem = true,并且 $banned 有值
return $banned === 1;
}
}

修改成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
<?php
/**
* [Discuz!] (C)2001-2099 Discuz! Team
* This is NOT a freeware, use is subject to license terms
* https://license.discuz.vip
*/
if(!defined('IN_DISCUZ')) {
exit('Access Denied');
}
class table_common_banned extends discuz_table {
/*
* 在memory启用的时候,存储于common_banned_index的SortedSet中
* member = ip的16进制表达, score = 1 表示封禁,score = 0 表示不封禁
* 对于 onion 地址,使用 MD5 哈希作为 member
*/
public static function t() {
static $_instance;
if(!isset($_instance)) {
$_instance = new self();
}
return $_instance;
}
public function __construct() {
$this->_table = 'common_banned';
$this->_pk = 'id';
$this->_pre_cache_key = 'common_banned_';
$this->_cache_ttl = 600;
parent::__construct();
$this->_allowmem = $this->_allowmem && C::memory()->gotsortedset;
}
public function fetch_by_ip($ip) {
return DB::fetch_first('SELECT * FROM %t WHERE ip=%s', [$this->_table, $ip]);
}
public function fetch_all_order_dateline() {
return DB::fetch_all('SELECT * FROM %t ORDER BY dateline DESC', [$this->_table]);
}
public function fetch_all($ids = [], $force_from_db = false) {
// Todo: $ids = array() 需要在取消兼容层后删除
if(defined('DISCUZ_DEPRECATED')) {
throw new Exception('NotImplementedException');
return parent::fetch_all($ids, $force_from_db);
} else {
return $this->fetch_all_banned();
}
}
public function fetch_all_banned() {
return DB::fetch_all('SELECT * FROM %t', [$this->_table]);
}
public function delete_by_id($ids, $adminid, $adminname) {
$ids = array_map('intval', (array)$ids);
if($ids) {
if($this->_allowmem) memory('rm', 'index', $this->_pre_cache_key);
return DB::query('DELETE FROM %t WHERE id IN(%n) AND (1=%d OR `admin`=%s)', [$this->_table, $ids, $adminid, $adminname]);
}
return 0;
}
public function update_expiration_by_id($id, $expiration, $isadmin, $admin) {
if($this->_allowmem) memory('rm', 'index', $this->_pre_cache_key);
return DB::query('UPDATE %t SET expiration=%d WHERE id=%d AND (1=%d OR `admin`=%s)', [$this->_table, $expiration, $id, $isadmin, $admin]);
}
public function insert($data, $return_insert_id = false, $replace = false, $silent = false) {
$cmd = $replace ? 'REPLACE INTO' : 'INSERT INTO';

// 检查是否是 onion 地址
if(method_exists('ip', 'validate_onion') && ip::validate_onion($data['ip'])) {
// onion 地址特殊处理 - 使用 MD5 哈希
$onion_hash = md5($data['ip']);
$data['lowerip'] = '0x' . $onion_hash;
$data['upperip'] = '0x' . $onion_hash;
} else {
// 传统 IP 地址处理
if(!str_starts_with($data['lowerip'], '0x')) $data['lowerip'] = '0x'.$data['lowerip'];
if(!str_starts_with($data['upperip'], '0x')) $data['upperip'] = '0x'.$data['upperip'];
}

if($this->_allowmem) memory('rm', 'index', $this->_pre_cache_key);
return DB::query(
$cmd.' %t SET `ip`=%s, `lowerip`=%i, `upperip`=%i, `admin`=%s, `dateline`=%d, `expiration`=%d',
[$this->_table, $data['ip'], $data['lowerip'], $data['upperip'], $data['admin'], $data['dateline'], $data['expiration']],
$silent, !$return_insert_id
);
}

public function check_banned($time_to_check, $ip) {
// 检查是否是 onion 地址
if(method_exists('ip', 'validate_onion') && ip::validate_onion($ip)) {
return $this->check_banned_onion($time_to_check, $ip);
}

// 传统 IP 地址处理
$iphex = ip::ip_to_hex_str($ip);

// 如果 IP 转换失败,返回 false(不封禁)
if($iphex === false || empty($iphex)) {
return false;
}

$banned = true;
if($this->_allowmem) $banned = memory('zscore', 'index', $iphex, 0, $this->_pre_cache_key);
if($banned === false || !$this->_allowmem) { // 如果memory中没有值,或不使用memory,都走数据库
$iphex_val = '0x'.$iphex;
$ret = DB::result_first(
'SELECT id from %t WHERE expiration > %d AND lowerip <= %i AND upperip >= %i',
[$this->_table, $time_to_check, $iphex_val, $iphex_val]
);
if($ret) {
if($this->_allowmem) memory('zadd', 'index', $iphex, 1, $this->_pre_cache_key);
return true;
}
if($this->_allowmem) memory('zadd', 'index', $iphex, 0, $this->_pre_cache_key);
return false;
}
// _allowmem = true,并且 $banned 有值
return $banned === 1;
}

/**
* 检查 onion 地址是否被封禁
*/
private function check_banned_onion($time_to_check, $onion_ip) {
// 使用 onion 地址的 MD5 哈希作为缓存键
$onion_hash = md5($onion_ip);

$banned = true;
if($this->_allowmem) $banned = memory('zscore', 'index', $onion_hash, 0, $this->_pre_cache_key);

if($banned === false || !$this->_allowmem) {
// 对 onion 地址进行精确匹配
$ret = DB::result_first(
'SELECT id from %t WHERE expiration > %d AND ip = %s',
[$this->_table, $time_to_check, $onion_ip]
);

if($ret) {
if($this->_allowmem) memory('zadd', 'index', $onion_hash, 1, $this->_pre_cache_key);
return true;
}
if($this->_allowmem) memory('zadd', 'index', $onion_hash, 0, $this->_pre_cache_key);
return false;
}

return $banned === 1;
}

/**
* 添加封禁记录
*/
public function add_ban($ip, $admin, $expiration, $reason = '') {
$data = [
'ip' => $ip,
'admin' => $admin,
'dateline' => TIMESTAMP,
'expiration' => $expiration
];

// 检查是否是 onion 地址
if(method_exists('ip', 'validate_onion') && ip::validate_onion($ip)) {
// onion 地址使用 MD5 哈希
$onion_hash = md5($ip);
$data['lowerip'] = $onion_hash;
$data['upperip'] = $onion_hash;
} else {
// 传统 IP 地址处理
$iphex = ip::ip_to_hex_str($ip);
if($iphex === false) {
return false; // IP 格式不正确
}
$data['lowerip'] = $iphex;
$data['upperip'] = $iphex;
}

return $this->insert($data, true);
}
}

主要修改内容:
1. 修复 check_banned 方法

添加了对 onion 地址的检测
如果是 onion 地址,调用专门的 check_banned_onion 方法
对传统 IP 地址,增加了错误检查,避免 ip_to_hex_str 返回 false 时的 SQL 错误

2. 新增 check_banned_onion 方法

专门处理 onion 地址的封禁检查
使用精确的字符串匹配而不是 IP 范围匹配
使用 MD5 哈希作为缓存键

3. 修改 insert 方法

自动检测 onion 地址并使用相应的处理逻辑
onion 地址使用 MD5 哈希填充 lowerip 和 upperip 字段

4. 新增 add_ban 方法

提供一个更友好的接口来添加封禁记录
自动处理不同类型地址的格式转换
这样修改后,Discuz! 就可以正确处理 onion 地址的访问,不会再出现 SQL 错误了。

八、启动所有服务

1
2
3
4
5
6
7
8
9
10
11
12
13
/usr/local/redis/bin/redis-server /usr/local/redis/redis.conf &
systemctl daemon-reload
systemctl restart nginx
systemctl restart php8.3-fpm
apparmor_parser -r /etc/apparmor.d/system_tor
systemctl restart tor
systemctl restart mariadb
systemctl enable nginx
systemctl enable php8.3-fpm
systemctl enable tor
cd /www/tor/dz
chown debian-tor:debian-tor dz.sock
cat /www/tor/dz/hostname

得到onion域名后在Tor浏览器打开,然后根据页面提示一步一步操作就行,我选择的是安装全新的Discuz!

总结

先安装 Nginxphpmariadbtorredis 这些中间件,然后进行配置,接着 git 源码,修改下 index.phpclass_ip.phptable_common_banned.php这三个文件来让Discuz!可以在Tor网络下正常访问

微信扫一扫关注我吧

戴戴的Linux 戴戴的Linux

文章目录
  1. 1. 前言
  2. 2. 仅限学习交流,不得用于任何违法犯罪用途,若用于违法犯罪用途,因此产生的法律后果需要你自行承担,博主不承担任何法律责任!
  3. 3. 仅限学习交流,不得用于任何违法犯罪用途,若用于违法犯罪用途,因此产生的法律后果需要你自行承担,博主不承担任何法律责任!
  4. 4. 仅限学习交流,不得用于任何违法犯罪用途,若用于违法犯罪用途,因此产生的法律后果需要你自行承担,博主不承担任何法律责任!
  5. 5. 一、环境准备
  6. 6. 二、安装和配置Nginx
  7. 7. 三、安装PHP8.3
  8. 8. 四、安装并初始化mariadb
  9. 9. 五、编译安装并配置Redis
  10. 10. 六、安装并配置Tor
  11. 11. 七、git项目并修改php代码
  12. 12. 八、启动所有服务
  13. 13. 总结
  14. 14. 微信扫一扫关注我吧


本站总访问量 本文总阅读量