Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0405277231 | ||
|
|
4191d8cbc1 | ||
|
|
999ac75324 | ||
|
|
d783b6c413 | ||
|
|
4ee668dc13 | ||
|
|
b3210882dd |
@@ -5,7 +5,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
api_version: 28 # min. 21
|
api_version: 28 # min. 21
|
||||||
ndk: r26d # android-ndk-$ndk-linux.zip
|
ndk: r28 # android-ndk-$ndk-linux.zip
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -97,3 +97,11 @@ log files are placed in `run`, `daemon.log` for `service.sh` and `zerotier.log`
|
|||||||
## Build binaries yourself
|
## Build binaries yourself
|
||||||
|
|
||||||
refer to `.github/workflow/build-{gcc|ndk}.yml` for detailed information.
|
refer to `.github/workflow/build-{gcc|ndk}.yml` for detailed information.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
After 1.14.0, ZeroTierOne has introduce `multi-core concurrent packet processing`, which requires `pthread_setaffinity_np`.
|
||||||
|
|
||||||
|
However, for NDK, `pthread_setaffinity_np` won't be available until API level 36, Android 16. (refer to https://android.googlesource.com/platform/bionic/+/master/libc/include/pthread.h)
|
||||||
|
|
||||||
|
So in the NDK bulid version, it is replaced by the combination of `pthread_gettid_np` from `<pthread.h>` and `sched_getaffinity` from `<sched.h>`.
|
||||||
@@ -97,3 +97,11 @@ ZeroTier 可执行文件和操作的 Shell 脚本放在 `/data/adb/zerotier/`
|
|||||||
## 自行编译
|
## 自行编译
|
||||||
|
|
||||||
参考 `.github/workflow/build-{gcc|ndk}.yml`
|
参考 `.github/workflow/build-{gcc|ndk}.yml`
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
1.14.0 后 ZeroTierOne 引入了 `multi-core concurrent packet processing` ,其中使用了 `pthread_setaffinity_np` 实现线程亲和性设置
|
||||||
|
|
||||||
|
但 `pthread_setaffinity_np` 在 NDK 的 API level 36, Android 16 才受支持。 (参考 https://android.googlesource.com/platform/bionic/+/master/libc/include/pthread.h)
|
||||||
|
|
||||||
|
所以 NDK 中他被替换成了 `<pthread.h>` 中 `pthread_gettid_np` 和 `<sched.h>` 中 `sched_getaffinity` 的组合,来实现相同的功能
|
||||||
+59
-17
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:quickalert/quickalert.dart';
|
||||||
|
|
||||||
import './authed_client.dart';
|
import './authed_client.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
@@ -126,8 +127,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
late Function restartFn, startFn, stopFn, joinFn, leaveFn;
|
late Function restartFn, startFn, stopFn, joinFn, leaveFn;
|
||||||
|
|
||||||
_MyHomePageState() : super() {
|
_MyHomePageState() : super() {
|
||||||
restartFn =
|
restartFn = cmdButtonWrapper(() => zerotierCommand('restart'));
|
||||||
cmdButtonWrapper(() => zerotierCommand('restart'));
|
|
||||||
startFn = cmdButtonWrapper(() => zerotierCommand('start'));
|
startFn = cmdButtonWrapper(() => zerotierCommand('start'));
|
||||||
stopFn = cmdButtonWrapper(() => zerotierCommand('stop'));
|
stopFn = cmdButtonWrapper(() => zerotierCommand('stop'));
|
||||||
joinFn = cmdButtonWrapper(() => joinNetwork(_idInputController.text));
|
joinFn = cmdButtonWrapper(() => joinNetwork(_idInputController.text));
|
||||||
@@ -141,37 +141,44 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
}
|
}
|
||||||
return Column(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
return Column(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
|
||||||
Builder(builder: (BuildContext context) {
|
Builder(builder: (BuildContext context) {
|
||||||
final color = (runningStatus ? Colors.green : Colors.red)[200];
|
final color = (runningStatus ? Colors.green : Colors.red)[300];
|
||||||
|
|
||||||
return Column(children: [
|
return Column(children: [
|
||||||
Icon(runningStatus ? Icons.play_arrow : Icons.stop, color: color, size: 72),
|
Icon(runningStatus ? Icons.play_arrow : Icons.stop,
|
||||||
|
color: color, size: 72),
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
Text(runningStatus ? 'Running' : 'Stopped', style: TextStyle(color: color, fontSize: 18)),
|
Text(runningStatus ? 'Running' : 'Stopped',
|
||||||
|
style: TextStyle(color: color, fontSize: 18)),
|
||||||
IconButton(icon: const Icon(Icons.refresh), onPressed: loadStatus)
|
IconButton(icon: const Icon(Icons.refresh), onPressed: loadStatus)
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
ButtonBar(
|
IntrinsicWidth(
|
||||||
alignment: MainAxisAlignment.center,
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
|
style: const ButtonStyle(alignment: Alignment.centerLeft),
|
||||||
onPressed: startFn(),
|
onPressed: startFn(),
|
||||||
label: const Text('Start'),
|
label: const Text('Start'),
|
||||||
icon: const Icon(Icons.play_arrow),
|
icon: const Icon(Icons.play_arrow),
|
||||||
),
|
),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
|
style: const ButtonStyle(alignment: Alignment.centerLeft),
|
||||||
onPressed: restartFn(),
|
onPressed: restartFn(),
|
||||||
label: const Text('Restart'),
|
label: const Text('Restart'),
|
||||||
icon: const Icon(Icons.restart_alt),
|
icon: const Icon(Icons.restart_alt),
|
||||||
),
|
),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
|
style: const ButtonStyle(alignment: Alignment.centerLeft),
|
||||||
onPressed: stopFn(),
|
onPressed: stopFn(),
|
||||||
label: const Text('Stop'),
|
label: const Text('Stop'),
|
||||||
icon: const Icon(Icons.stop),
|
icon: const Icon(Icons.stop),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
))
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +203,14 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: "network id", icon: Icon(Icons.router)))),
|
labelText: "network id", icon: Icon(Icons.router)))),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: joinFn(),
|
onPressed: () {
|
||||||
|
joinFn()();
|
||||||
|
QuickAlert.show(
|
||||||
|
context: context,
|
||||||
|
type: QuickAlertType.success,
|
||||||
|
text: 'Joined ${_idInputController.text}',
|
||||||
|
);
|
||||||
|
},
|
||||||
label: const Text('Join'),
|
label: const Text('Join'),
|
||||||
icon: const Icon(Icons.add)),
|
icon: const Icon(Icons.add)),
|
||||||
]),
|
]),
|
||||||
@@ -231,15 +245,30 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
width: 96,
|
width: 96,
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Clipboard.setData(
|
onPressed: () {
|
||||||
ClipboardData(
|
Clipboard.setData(ClipboardData(
|
||||||
text: networkList[i]))
|
text: networkList[i]));
|
||||||
.then,
|
QuickAlert.show(
|
||||||
|
context: context,
|
||||||
|
type: QuickAlertType.success,
|
||||||
|
text: 'Copied to clipboard!',
|
||||||
|
);
|
||||||
|
},
|
||||||
tooltip: '复制',
|
tooltip: '复制',
|
||||||
icon: const Icon(Icons.copy)),
|
icon: const Icon(Icons.copy)),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () =>
|
onPressed: () => QuickAlert.show(
|
||||||
leaveNetwork(networkList[i]),
|
context: context,
|
||||||
|
type: QuickAlertType.confirm,
|
||||||
|
text:
|
||||||
|
'Delete network ${networkList[i]} ?',
|
||||||
|
confirmBtnText: 'Yes',
|
||||||
|
cancelBtnText: 'No',
|
||||||
|
confirmBtnColor: Colors.green,
|
||||||
|
onConfirmBtnTap: () {
|
||||||
|
leaveNetwork(networkList[i]);
|
||||||
|
Navigator.pop(context);
|
||||||
|
}),
|
||||||
tooltip: '离开',
|
tooltip: '离开',
|
||||||
icon: const Icon(Icons.close)),
|
icon: const Icon(Icons.close)),
|
||||||
]))));
|
]))));
|
||||||
@@ -252,11 +281,19 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
peersPage({bool title = false}) {
|
||||||
|
if (title) {
|
||||||
|
return 'Peers';
|
||||||
|
}
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
|
||||||
int currentPageIndex = 0;
|
int currentPageIndex = 0;
|
||||||
genWidgetList({bool title = false}) {
|
genWidgetList({bool title = false}) {
|
||||||
return [
|
return [
|
||||||
homePage(title: title),
|
homePage(title: title),
|
||||||
networkPage(title: title)
|
networkPage(title: title),
|
||||||
|
peersPage(title: title)
|
||||||
][currentPageIndex];
|
][currentPageIndex];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,6 +319,11 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
icon: Icon(Icons.router_outlined),
|
icon: Icon(Icons.router_outlined),
|
||||||
label: 'Network',
|
label: 'Network',
|
||||||
),
|
),
|
||||||
|
NavigationDestination(
|
||||||
|
selectedIcon: Icon(Icons.account_box),
|
||||||
|
icon: Icon(Icons.account_box_outlined),
|
||||||
|
label: 'Peers',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
|
|||||||
+20
-12
@@ -175,26 +175,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.0"
|
version: "10.0.4"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "3.0.3"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "3.0.1"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -223,10 +223,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.0"
|
version: "1.12.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -307,6 +307,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
quickalert:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: quickalert
|
||||||
|
sha256: b5d62b1e20b08cc0ff5f40b6da519bdc7a5de6082f13d90572cf4e72eea56c5e
|
||||||
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -356,10 +364,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
|
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.1"
|
version: "0.7.0"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -380,10 +388,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "13.0.0"
|
version: "14.2.1"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ dependencies:
|
|||||||
cupertino_icons: ^1.0.6
|
cupertino_icons: ^1.0.6
|
||||||
http: ^1.2.1
|
http: ^1.2.1
|
||||||
path_provider: ^2.1.3
|
path_provider: ^2.1.3
|
||||||
|
quickalert: ^1.1.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
+26
-9
@@ -28,6 +28,25 @@ with open(config_toml_path, 'w') as f:
|
|||||||
|
|
||||||
# -------------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Patch ZeroTierOne/osdep/LinuxEthernetTap.cpp
|
||||||
|
|
||||||
|
linux_tap_path = 'ZeroTierOne/osdep/LinuxEthernetTap.cpp'
|
||||||
|
|
||||||
|
linux_tap_match1 = 'int rc = pthread_setaffinity_np(self, sizeof(cpu_set_t), &cpuset);'
|
||||||
|
linux_tap_replace1 = 'int rc = sched_setaffinity(pthread_gettid_np(self), sizeof(cpu_set_t), &cpuset);'
|
||||||
|
|
||||||
|
linux_tap_match2 = '#include <sys/utsname.h>'
|
||||||
|
linux_tap_replace2 = '#include <sys/utsname.h>\n#include <sched.h>'
|
||||||
|
|
||||||
|
with open(linux_tap_path, 'r') as file:
|
||||||
|
data = file.read()
|
||||||
|
patch_NDK = data.replace(linux_tap_match1, linux_tap_replace1).replace(linux_tap_match2, linux_tap_replace2)
|
||||||
|
|
||||||
|
with open(linux_tap_path, 'w') as file:
|
||||||
|
file.write(patch_NDK)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
# Patch make-linux.mk
|
# Patch make-linux.mk
|
||||||
|
|
||||||
make_linux_path = 'ZeroTierOne/make-linux.mk'
|
make_linux_path = 'ZeroTierOne/make-linux.mk'
|
||||||
@@ -57,19 +76,17 @@ with open(make_linux_path, 'r') as file:
|
|||||||
'override CFLAGS+=-march=armv7-a -marm -mfpu=vfp -fexceptions'
|
'override CFLAGS+=-march=armv7-a -marm -mfpu=vfp -fexceptions'
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(make_linux_path + '.aarch64', 'w') as file:
|
with open(make_linux_path + '.aarch64', 'w') as file:
|
||||||
file.write(patch_aarch64)
|
file.write(patch_aarch64)
|
||||||
with open(make_linux_path + '.arm', 'w') as file:
|
with open(make_linux_path + '.arm', 'w') as file:
|
||||||
file.write(patch_arm)
|
file.write(patch_arm)
|
||||||
with open(make_linux_path + '.arm.ndk', 'w') as file:
|
with open(make_linux_path + '.arm.ndk', 'w') as file:
|
||||||
file.write(patch_arm_ndk)
|
file.write(patch_arm_ndk)
|
||||||
|
|
||||||
# Patch ZeroTierOne/osdep/OSUtils.cpp
|
# Patch ZeroTierOne/osdep/OSUtils.cpp
|
||||||
|
|
||||||
osutil_path = 'ZeroTierOne/osdep/OSUtils.cpp'
|
osutil_path = 'ZeroTierOne/osdep/OSUtils.cpp'
|
||||||
with open(osutil_path, 'r') as file:
|
with open(osutil_path, 'r') as file:
|
||||||
data = file.read().replace(
|
data = file.read().replace('/var/lib/zerotier-one', '/data/adb/zerotier/home')
|
||||||
'/var/lib/zerotier-one', '/data/adb/zerotier/home'
|
|
||||||
)
|
|
||||||
with open(osutil_path, 'w') as file:
|
with open(osutil_path, 'w') as file:
|
||||||
file.write(data)
|
file.write(data)
|
||||||
@@ -21,6 +21,11 @@ _stop() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
kill -9 $pid
|
kill -9 $pid
|
||||||
|
|
||||||
|
# delete from ip rules
|
||||||
|
ip rule del from all lookup main pref 1
|
||||||
|
ip -6 rule del from all lookup main pref 1
|
||||||
|
|
||||||
if [[ $? -ne 0 ]]; then
|
if [[ $? -ne 0 ]]; then
|
||||||
log "kill zerotier-one failed"
|
log "kill zerotier-one failed"
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,5 +13,10 @@ PIPE_APP=$APPROOT/run/pipe
|
|||||||
export LD_LIBRARY_PATH=/system/lib64:/data/adb/zerotier/lib
|
export LD_LIBRARY_PATH=/system/lib64:/data/adb/zerotier/lib
|
||||||
|
|
||||||
__start() {
|
__start() {
|
||||||
|
# add main route table to lookup rules
|
||||||
|
ip rule add from all lookup main pref 1
|
||||||
|
ip -6 rule add from all lookup main pref 1
|
||||||
|
|
||||||
|
# start zerotier daemon
|
||||||
nohup $ZTROOT/zerotier-one -d >> $ZT_LOG 2>&1 &
|
nohup $ZTROOT/zerotier-one -d >> $ZT_LOG 2>&1 &
|
||||||
}
|
}
|
||||||
@@ -33,11 +33,6 @@ mkdir -p $ZTROOT/run
|
|||||||
# start zerotier
|
# start zerotier
|
||||||
# ----------------------------------------------
|
# ----------------------------------------------
|
||||||
|
|
||||||
# add main route table to lookup rules
|
|
||||||
ip rule add from all lookup main pref 1
|
|
||||||
ip -6 rule add from all lookup main pref 1
|
|
||||||
|
|
||||||
# start zerotier
|
|
||||||
__start
|
__start
|
||||||
|
|
||||||
# ----------------------------------------------
|
# ----------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user