mirror of
https://github.com/srlabs/blue-merle.git
synced 2025-01-18 18:47:40 -05:00
Initial commit
This commit is contained in:
commit
cb4d73731f
BIN
Documentation.pdf
Normal file
BIN
Documentation.pdf
Normal file
Binary file not shown.
BIN
IMEI randomization.png
Normal file
BIN
IMEI randomization.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 906 KiB |
29
LICENSE.md
Normal file
29
LICENSE.md
Normal file
@ -0,0 +1,29 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2022, SRLabs
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
137
Makefile
Normal file
137
Makefile
Normal file
@ -0,0 +1,137 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=blue-merle
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=$(AUTORELEASE)
|
||||
|
||||
PKG_MAINTAINER:=Matthias <matthias@srlabs.de>
|
||||
PKG_LICENSE:=BSD-3-Clause
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/blue-merle
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
DEPENDS:=+bash +coreutils-shred +python3 +python3-pyserial +patch
|
||||
TITLE:=Anonymity Enhancements for GL-E750 Mudi
|
||||
endef
|
||||
|
||||
define Package/blue-merle/description
|
||||
The blue-merle package enhances anonymity and reduces forensic traceability of the GL-E750 Mudi 4G mobile wi-fi router
|
||||
endef
|
||||
|
||||
define Build/Configure
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/blue-merle/install
|
||||
$(CP) ./files/* $(1)/
|
||||
$(INSTALL_BIN) ./files/etc/init.d/* $(1)/etc/init.d/
|
||||
$(INSTALL_BIN) ./files/lib/blue-merle/mac-wipe.sh $(1)/lib/blue-merle/mac-wipe.sh
|
||||
$(INSTALL_BIN) ./files/usr/bin/blue-merle $(1)/usr/bin/blue-merle
|
||||
endef
|
||||
|
||||
define Package/blue-merle/preinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0 # if run within buildroot exit
|
||||
|
||||
ABORT_GLVERSION () {
|
||||
echo
|
||||
if [ -f "/tmp/sysinfo/model" ] && [ -f "/etc/glversion" ]; then
|
||||
echo "You have a `cat /tmp/sysinfo/model`, running firmware version `cat /etc/glversion`."
|
||||
fi
|
||||
echo "blue-merle has only been tested with GL-E750 Mudi Version 3.215."
|
||||
echo "The device or firmware version you are using have not been verified to work with blue-merle."
|
||||
echo -n "Would you like to continue on your own risk? (y/N): "
|
||||
read answer
|
||||
case $$answer in
|
||||
y*) answer=0;;
|
||||
y*) answer=0;;
|
||||
*) answer=1;;
|
||||
esac
|
||||
if [[ "$$answer" -eq 0 ]]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
UPDATE_MCU() {
|
||||
echo "6e6b86e3ad7fec0d5e426eb9a41c51c6f0d6b68a4d341ec553edeeade3e4b470 /tmp/e750-mcu-V1.0.7.bin" > /tmp/e750-mcu.bin.sha256
|
||||
wget -O /tmp/e750-mcu-V1.0.7.bin https://github.com/gl-inet/GL-E750-MCU-instruction/blob/master/e750-mcu-V1.0.7-56a1cad7f0eb8318ebe3c3c46a4cf3ff.bin?raw=true
|
||||
if sha256sum -cs /tmp/e750-mcu.bin.sha256; then
|
||||
ubus call service delete '{"name":"e750_mcu"}'
|
||||
mcu_update /tmp/e750-mcu-V1.0.7.bin
|
||||
else
|
||||
echo "Failed to update MCU, verification of the binary failed."
|
||||
echo "Your device needs to be connected to the Internet in order to download the MCU binary."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
CHECK_MCUVERSION() {
|
||||
function version { echo "$$@" | cut -d' ' -f2 | awk -F. '{ printf("%d%03d%03d%03d\n", $$1,$$2,$$3,$$4); }'; }
|
||||
mcu_version=`echo \{\"version\": \"1\"} > /dev/ttyS0; sleep 0.1; cat /dev/ttyS0|tr -d '\n'`
|
||||
if [ $$(version "$$mcu_version") -ge $$(version "V 1.0.7") ]; then
|
||||
return 0
|
||||
else
|
||||
echo
|
||||
echo "Your MCU version has not been verified to work with blue-merle."
|
||||
echo "Automatic shutdown may not work."
|
||||
echo "The install script can initiate an update of the MCU."
|
||||
echo "The device will reboot and, after reboot, you need to run opkg install blue-merle again."
|
||||
echo -n "Would you like to update your MCU? (y/N): "
|
||||
read answer
|
||||
case $$answer in
|
||||
Y*) answer=0;;
|
||||
y*) answer=0;;
|
||||
*) answer=1;;
|
||||
esac
|
||||
if [[ "$$answer" -eq 0 ]]; then
|
||||
UPDATE_MCU
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if grep -q "GL.iNet GL-E750" /proc/cpuinfo; then
|
||||
if grep -q -w "3.215" /etc/glversion; then
|
||||
CHECK_MCUVERSION
|
||||
echo "Device is supported, installing blue-merle."
|
||||
exit 0
|
||||
else
|
||||
ABORT_GLVERSION
|
||||
fi
|
||||
else
|
||||
ABORT_GLVERSION
|
||||
fi
|
||||
endef
|
||||
|
||||
define Package/blue-merle/postinst
|
||||
#!/bin/sh
|
||||
|
||||
patch -b /www/src/temple/settings/index.js /lib/blue-merle/patches/index.js.patch
|
||||
patch -b /www/src/temple/settings/index.html /lib/blue-merle/patches/index.html.patch
|
||||
patch -b /usr/bin/switchaction /lib/blue-merle/patches/switchaction.patch
|
||||
patch -b /usr/bin/switch_queue /lib/blue-merle/patches/switch_queue.patch
|
||||
|
||||
uci set glconfig.switch_button='service'
|
||||
uci set glconfig.switch_button.enable='1'
|
||||
uci set glconfig.switch_button.function='sim'
|
||||
uci commit glconfig
|
||||
endef
|
||||
|
||||
define Package/blue-merle/postrm
|
||||
#!/bin/sh
|
||||
|
||||
mv /www/src/temple/settings/index.js.orig /www/src/temple/settings/index.js
|
||||
mv /www/src/temple/settings/index.html.orig /www/src/temple/settings/index.html
|
||||
mv /usr/bin/switchaction.orig /usr/bin/switchaction
|
||||
mv /usr/bin/switch_queue.orig /usr/bin/switch_queue
|
||||
|
||||
rm /tmp/sim_change_start
|
||||
rm /tmp/sim_change_switch
|
||||
endef
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
|
86
README.md
Normal file
86
README.md
Normal file
@ -0,0 +1,86 @@
|
||||
# blue-merle
|
||||
|
||||
The *blue-merle* software package enhances anonymity and reduces forensic traceability of the **GL-E750 / Mudi 4G mobile wi-fi router ("Mudi router")**. The portable device is explicitly marketed to privacy-interested retail users.
|
||||
|
||||
*blue-merle* addresses the traceability drawbacks of the Mudi router by adding the following features to the Mudi router:
|
||||
|
||||
1. Mobile Equipment Identity (IMEI) changer
|
||||
|
||||
2. Media Access Control (MAC) address log wiper
|
||||
|
||||
3. Basic Service Set Identifier (BSSID) randomization
|
||||
|
||||
## Installing prebuild package
|
||||
|
||||
Download the [prebuild package](https://github.com/srlabs/blue-merle/releases) and copy it onto your Mudi, preferably into the /tmp folder. Then install the package file:
|
||||
|
||||
```
|
||||
opkg update
|
||||
opkg install blue-merle*.ipk
|
||||
```
|
||||
|
||||
Now you may initiate an IMEI update on the command line by running `blue-merle` or by using Mudi's toggle button. Both the command line and hardware button version of *blue-merle* will guide you through the IMEI update process in order to minimize the risk of IMEI leaks.
|
||||
|
||||
The *blue-merle* package has been verified to work with GL-E750 Mudi version 3.215. A MCU version >= 1.0.7 is required. The MCU may be updated through the *blue-merle* package installer or [manually](https://github.com/gl-inet/GL-E750-MCU-instruction). SRLabs cannot guarantee that the project assets within this Git repository will be compatible with future firmware updates.
|
||||
|
||||
## Build package
|
||||
```
|
||||
git clone https://github.com/openwrt/openwrt
|
||||
cd openwrt
|
||||
git clone https://github.com/srlabs/blue-merle package/blue-merle
|
||||
./scripts/feeds update -a && ./scripts/feeds install -a
|
||||
make distclean && make clean
|
||||
make menuconfig
|
||||
# Target System: Atheros ATH79
|
||||
# Subtarget Generic Device with NAND flash
|
||||
# Target Profile: GL.iNet GL-E750
|
||||
# In Utilities, select <M> for blue-merle package
|
||||
# Save new configuration
|
||||
make
|
||||
make package/blue-merle/compile
|
||||
```
|
||||
You will find the package in `./bin/packages/mips_24kc/base/`
|
||||
|
||||
## Implementation details
|
||||
|
||||
### IMEI randomization
|
||||
|
||||
The Mudi router's baseband unit is a Quectel EP06-E/A Series LTE Cat 6 Mini PCIe [module](https://www.quectel.com/wp-content/uploads/pdfupload/Quectel_EP06_Series_LTE-A_Specification_V1.7.pdf>).
|
||||
|
||||
The Mudi router's IMEI can be changed by issuing Quectel LTE series-standard AT commands. The AT command to write a new IMEI to a Quectel EP06-E/A-based device is `AT+EGMR`.
|
||||
|
||||
Our IMEI randomization functionality is built around this command and implements two approaches to IMEI generation. The first deterministic method seeds the new value with the user's ISMI, while the second generates a random IMEI.
|
||||
|
||||
To change the IMEI on the command-line, run `blue-merle` and follow the instructions. Alternatively, you can use the hardware switch button to set a random IMEI.
|
||||
|
||||
SRLabs researchers verified that the Mudi router's IMEI can be changed persistently by connecting the device to a custom telco base station set-up. The changed IMEI is recorded within the new base station database entry, confirming that the IMEI change is observed both on the device- and ISP-level.
|
||||
|
||||
Furthermore, to ensure that there is no leakage of the old IMEI after changing the SIM card and setting a new IMEI, the Mudi router's radio is turned off in advance. Both the command-line and hardware switch version of *blue-merle* will guide you through the IMEI update process in order to minimize the risk of IMEI leaks.
|
||||
|
||||
Running *blue-merle* will disrupt the device's connection with the ISP during the time the IMEI is changed, and by default the connection is only reestablished once the device is rebooted.
|
||||
|
||||
This process can be observed in Figure 1, where there is a large break in connectivity between entries 70 and 80. This break is the result of turning the radio off.
|
||||
|
||||
![Figure 1. The router's radio is turned off and the IMEI is randomized between entries 70 and 80. The ISP cannot connect to it.](https://github.com/srlabs/blue-merle/blob/main/IMEI%20randomization.png)
|
||||
|
||||
[Figure 1](https://github.com/srlabs/blue-merle/blob/main/IMEI%20randomization.png) The router's radio is turned off and the IMEI is randomized between entries 70 and 80. The ISP cannot connect to it.
|
||||
|
||||
### Basic Service Set Identifier (BSSID) randomization
|
||||
|
||||
The Mudi router BSSID is set by the hostapd process using the `mac80211_prepare_vif()` function in `/rom/lib/netifd/wireless/mac80211.sh`. The resulting BSSID is stored in `/etc/config/wireless`.
|
||||
|
||||
The implemented BSSID randomization function generates a valid unicast address value and overrides the current MAC values set within the `wlan0` and `wlan1` interfaces. This is done by issuing OpenWrt uci set commands targeting the macaddr fields of `wireless.@wifi-iface[0]` and `wireless.@wifi-iface[1]`. The Mudi router's wifi is then reset to implement the changes.
|
||||
|
||||
The BSSID randomization feature is run on boot, ensuring that a new BSSID is generated each time the device is started.
|
||||
|
||||
### MAC address log wiping
|
||||
|
||||
Connecting devices' MAC addresses are stored within the Mudi router at `/tmp/tertf(_bak)` and `/etc/tertf(_bak)`. The MAC address log wiper first symbolically links the gl_tertf file responsible for the gltertf process, which reads and writes MAC addresses to the above-mentioned directories. It then kills the gltertf process if active, checks if either directory contains any data, and uses shred to delete any data if found.
|
||||
|
||||
The MAC address log wiper is run on boot, ensuring that the Mudi device's initial MAC read/write functionality is disrupted each time the device is started.
|
||||
|
||||
## Acknowledgement: blue merle
|
||||
|
||||
The Mudi device shares a name with a Hungarian dog breed typically used to guard and herd flocks of livestock. Mudi dogs are agile, fast-learners and extremely friendly.
|
||||
|
||||
"Blue merle" is one of the five coat colours recognized for the Mudi dog breed by the Federation Cynologique Internationale and is characterized by its mottled or patched appearance. The black splashes on the blueish-gray coat of the blue merle Mudi inspired the name of this project because of its obscuring appearance and camouflaging symbolism.
|
0
files/etc/config/blue-merle
Normal file
0
files/etc/config/blue-merle
Normal file
17
files/etc/init.d/blue-merle
Executable file
17
files/etc/init.d/blue-merle
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
. /lib/blue-merle/functions.sh
|
||||
|
||||
START=81
|
||||
STOP=99
|
||||
|
||||
start() {
|
||||
/lib/blue-merle/mac-wipe.sh
|
||||
CHECKMACSYMLINK
|
||||
RESET_BSSIDS
|
||||
}
|
||||
|
||||
stop() {
|
||||
/lib/blue-merle/mac-wipe.sh
|
||||
}
|
||||
|
92
files/lib/blue-merle/functions.sh
Normal file
92
files/lib/blue-merle/functions.sh
Normal file
@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env ash
|
||||
|
||||
# This script provides helper functions for blue-merle
|
||||
|
||||
# check that MAC wiping/linking to dev/null is still in place
|
||||
CHECKMACSYMLINK () {
|
||||
local loc_file="/etc/init.d/gl_tertf"
|
||||
if [ $(readlink -f "$loc_file") == "/dev/null" ]
|
||||
then
|
||||
echo "TEST: EXISTS"
|
||||
else
|
||||
echo "TEST: DOES NOT EXIST"
|
||||
cp "$loc_file" "$loc_file.bak" # todo: consider if we need to move this backup elsewhere?
|
||||
ln -sf /dev/null "$loc_file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Restore gl_tertf from back-up
|
||||
RESTORE_GL_TERTF () {
|
||||
local loc_file="/etc/init.d/gl_tertf"
|
||||
local loc_backup="/etc/init.d/gl_tertf.bak"
|
||||
#local loc_location="/etc/init.d"
|
||||
rm "$loc_file"
|
||||
mv "$loc_backup" "$loc_file"
|
||||
}
|
||||
|
||||
UNICAST_MAC_GEN () {
|
||||
loc_mac_numgen=`python3 -c "import random; print(f'{random.randint(0,2**48) & 0b111111101111111111111111111111111111111111111111:0x}'.zfill(12))"`
|
||||
loc_mac_formatted=$(echo "$loc_mac_numgen" | sed 's/^\(..\)\(..\)\(..\)\(..\)\(..\)\(..\).*$/\1:\2:\3:\4:\5:\6/')
|
||||
echo "$loc_mac_formatted"
|
||||
}
|
||||
|
||||
# randomize BSSID
|
||||
RESET_BSSIDS () {
|
||||
uci set wireless.@wifi-iface[1].macaddr=`UNICAST_MAC_GEN`
|
||||
uci set wireless.@wifi-iface[0].macaddr=`UNICAST_MAC_GEN`
|
||||
uci commit wireless
|
||||
wifi # need to reset wifi for changes to apply
|
||||
}
|
||||
|
||||
READ_IMEI () {
|
||||
local answer=1
|
||||
while [[ "$answer" -eq 1 ]]; do
|
||||
local imei=$(gl_modem AT AT+GSN | grep -w -E "[0-9]{14,15}")
|
||||
if [[ $? -eq 1 ]]; then
|
||||
echo -n "Failed to read IMEI. Try again? (Y/n): "
|
||||
read answer
|
||||
case $answer in
|
||||
n*) answer=0;;
|
||||
N*) answer=0;;
|
||||
*) answer=1;;
|
||||
esac
|
||||
if [[ $answer -eq 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
answer=0
|
||||
fi
|
||||
done
|
||||
echo $imei
|
||||
}
|
||||
|
||||
READ_IMSI () {
|
||||
local answer=1
|
||||
while [[ "$answer" -eq 1 ]]; do
|
||||
local imsi=$(gl_modem AT AT+CIMI | grep -w -E "[0-9]{6,15}")
|
||||
if [[ $? -eq 1 ]]; then
|
||||
echo -n "Failed to read IMSI. Try again? (Y/n): "
|
||||
read answer
|
||||
case $answer in
|
||||
n*) answer=0;;
|
||||
N*) answer=0;;
|
||||
*) answer=1;;
|
||||
esac
|
||||
if [[ $answer -eq 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
answer=0
|
||||
fi
|
||||
done
|
||||
echo $imsi
|
||||
}
|
||||
|
||||
CHECK_ABORT () {
|
||||
sim_change_switch=`cat /tmp/sim_change_switch`
|
||||
if [[ "$sim_change_switch" = "off" ]]; then
|
||||
e750-mcu "SIM change aborted."
|
||||
sleep 1
|
||||
exit 1
|
||||
fi
|
||||
}
|
198
files/lib/blue-merle/imei_generate.py
Normal file
198
files/lib/blue-merle/imei_generate.py
Normal file
@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
import random
|
||||
import string
|
||||
import argparse
|
||||
import serial
|
||||
import re
|
||||
from functools import reduce
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Modes(Enum):
|
||||
DETERMINISTIC = 1
|
||||
RANDOM = 2
|
||||
STATIC = 3
|
||||
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("-v", "--verbose", help="Enables verbose output",
|
||||
action="store_true")
|
||||
modes = ap.add_mutually_exclusive_group()
|
||||
modes.add_argument("-d", "--deterministic", help="Switches IMEI generation to deterministic mode", action="store_true")
|
||||
modes.add_argument("-s", "--static", help="Sets user-defined IMEI",
|
||||
action="store")
|
||||
modes.add_argument("-r", "--random", help="Sets random IMEI",
|
||||
action="store_true")
|
||||
|
||||
# Example IMEI: 490154203237518
|
||||
imei_length = 14 # without validation digit
|
||||
# IDEA: make prefix configurable via CLI option
|
||||
imei_prefix = ["35674108", "35290611", "35397710", "35323210", "35384110",
|
||||
"35982748", "35672011", "35759049", "35266891", "35407115",
|
||||
"35538025", "35480910", "35324590", "35901183", "35139729",
|
||||
"35479164"]
|
||||
|
||||
verbose = False
|
||||
mode = None
|
||||
|
||||
# Serial global vars
|
||||
TTY = '/dev/ttyUSB3'
|
||||
BAUDRATE = 9600
|
||||
TIMEOUT = 3
|
||||
|
||||
|
||||
def get_imsi():
|
||||
with serial.Serial(TTY, BAUDRATE, timeout=TIMEOUT, exclusive=True) as ser:
|
||||
ser.write(b'AT+CIMI\r')
|
||||
# TODO: read loop until we have 'enough' of what to expect
|
||||
output = ser.read(64)
|
||||
|
||||
if (verbose):
|
||||
print(b'Output of AT+CIMI (Retrieve IMSI) command: ' + output)
|
||||
print('Output is of type: ' + str(type(output)))
|
||||
imsi_d = re.findall(b'[0-9]{15}', output)
|
||||
if (verbose):
|
||||
print("TEST: Read IMSI is", imsi_d)
|
||||
|
||||
return b"".join(imsi_d)
|
||||
|
||||
|
||||
def set_imei(imei):
|
||||
with serial.Serial(TTY, BAUDRATE, timeout=TIMEOUT, exclusive=True) as ser:
|
||||
cmd = b'AT+EGMR=1,7,\"'+imei.encode()+b'\"\r'
|
||||
ser.write(cmd)
|
||||
output = ser.read(64)
|
||||
|
||||
if (verbose):
|
||||
print(cmd)
|
||||
print(b'Output of AT+EGMR (Set IMEI) command: ' + output)
|
||||
print('Output is of type: ' + str(type(output)))
|
||||
|
||||
new_imei = get_imei()
|
||||
if (verbose):
|
||||
print(b"New IMEI: "+new_imei+b" Old IMEI: "+imei.encode())
|
||||
|
||||
if new_imei == imei.encode():
|
||||
print("IMEI has been successfully changed.")
|
||||
return True
|
||||
else:
|
||||
print("IMEI has not been successfully changed.")
|
||||
return False
|
||||
|
||||
|
||||
def get_imei():
|
||||
with serial.Serial(TTY, BAUDRATE, timeout=TIMEOUT, exclusive=True) as ser:
|
||||
ser.write(b'AT+GSN\r')
|
||||
output = ser.read(64)
|
||||
|
||||
if (verbose):
|
||||
print(b'Output of AT+GSN (Retrieve IMEI) command: ' + output)
|
||||
print('Output is of type: ' + str(type(output)))
|
||||
imei_d = re.findall(b'[0-9]{15}', output)
|
||||
if (verbose):
|
||||
print("TEST: Read IMEI is", imei_d)
|
||||
|
||||
return b"".join(imei_d)
|
||||
|
||||
|
||||
def generate_imei(imei_prefix, imsi_d):
|
||||
# In deterministic mode we seed the RNG with the IMSI.
|
||||
# As a consequence we will always generate the same IMEI for a given IMSI
|
||||
if (mode == Modes.DETERMINISTIC):
|
||||
random.seed(imsi_d)
|
||||
|
||||
# We choose a random prefix from the predefined list.
|
||||
# Then we fill the rest with random characters
|
||||
imei = random.choice(imei_prefix)
|
||||
if (verbose):
|
||||
print(f"IMEI prefix: {imei}")
|
||||
random_part_length = imei_length - len(imei)
|
||||
if (verbose):
|
||||
print(f"Length of the random IMEI part: {random_part_length}")
|
||||
imei += "".join(random.sample(string.digits, random_part_length))
|
||||
if (verbose):
|
||||
print(f"IMEI without validation digit: {imei}")
|
||||
|
||||
# calculate validation digit
|
||||
# Double each second digit in the IMEI: 4 18 0 2 5 8 2 0 3 4 3 14 5 2
|
||||
# (excluding the validation digit)
|
||||
|
||||
iteration_1 = "".join([c if i % 2 == 0 else str(2*int(c)) for i, c in enumerate(imei)])
|
||||
|
||||
# Separate this number into single digits: 4 1 8 0 2 5 8 2 0 3 4 3 1 4 5 2
|
||||
# (notice that 18 and 14 have been split).
|
||||
# Add up all the numbers: 4+1+8+0+2+5+8+2+0+3+4+3+1+4+5+2 = 52
|
||||
|
||||
sum = reduce((lambda a, b: int(a) + int(b)), iteration_1)
|
||||
|
||||
# Take your resulting number, remember it, and round it up to the nearest
|
||||
# multiple of ten: 60.
|
||||
# Subtract your original number from the rounded-up number: 60 - 52 = 8.
|
||||
|
||||
validation_digit = (10 - int(str(sum)[-1])) % 10
|
||||
if (verbose):
|
||||
print(f"Validation digit: {validation_digit}")
|
||||
|
||||
imei = str(imei) + str(validation_digit)
|
||||
if (verbose):
|
||||
print(f"Resulting IMEI: {imei}")
|
||||
|
||||
return imei
|
||||
|
||||
|
||||
def validate_imei(imei):
|
||||
# cut off last digit
|
||||
validation_digit = int(imei[-1])
|
||||
imei_verify = imei[0:14]
|
||||
if (verbose):
|
||||
print(imei_verify)
|
||||
|
||||
# Double each second digit in the IMEI
|
||||
iteration_1 = "".join([c if i % 2 == 0 else str(2*int(c)) for i, c in enumerate(imei_verify)])
|
||||
|
||||
# Separate this number into single digits and add them up
|
||||
sum = reduce((lambda a, b: int(a) + int(b)), iteration_1)
|
||||
if (verbose):
|
||||
print(sum)
|
||||
|
||||
# Take your resulting number, remember it, and round it up to the nearest
|
||||
# multiple of ten.
|
||||
# Subtract your original number from the rounded-up number.
|
||||
validation_digit_verify = (10 - int(str(sum)[-1])) % 10
|
||||
if (verbose):
|
||||
print(validation_digit_verify)
|
||||
|
||||
if validation_digit == validation_digit_verify:
|
||||
print(f"{imei} is CORRECT")
|
||||
return True
|
||||
|
||||
print(f"NOT A VALID IMEI: {imei}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = ap.parse_args()
|
||||
if args.verbose:
|
||||
verbose = args.verbose
|
||||
if args.deterministic:
|
||||
mode = Modes.DETERMINISTIC
|
||||
if args.random:
|
||||
mode = Modes.RANDOM
|
||||
if args.static is not None:
|
||||
mode = Modes.STATIC
|
||||
static_imei = args.static
|
||||
|
||||
if mode == Modes.STATIC:
|
||||
if validate_imei(static_imei):
|
||||
set_imei(static_imei)
|
||||
else:
|
||||
exit(-1)
|
||||
else:
|
||||
imsi_d = get_imsi()
|
||||
imei = generate_imei(imei_prefix, imsi_d)
|
||||
if (verbose):
|
||||
print(f"Generated new IMEI: {imei}")
|
||||
if not set_imei(imei):
|
||||
exit(-1)
|
||||
|
||||
exit(0)
|
71
files/lib/blue-merle/mac-wipe.sh
Normal file
71
files/lib/blue-merle/mac-wipe.sh
Normal file
@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env ash
|
||||
|
||||
# This script wipes all MAC address data from the device and is called upon boot
|
||||
|
||||
tmp_dir="/tmp/tertf"
|
||||
tmp_file="/tmp/tertf/tertfinfo_bak"
|
||||
|
||||
etc_dir="/etc/tertf"
|
||||
etc_file="/etc/tertf/tertfinfo_bak"
|
||||
|
||||
# Check for directories
|
||||
CHECKDIR_TMP () {
|
||||
if [ -d "$tmp_dir" ]; then
|
||||
echo "The /tmp/ directory exists."
|
||||
else
|
||||
echo "The /tmp/ directory does not exist. This should be fine..."
|
||||
fi
|
||||
}
|
||||
|
||||
CHECKDIR_ETC () {
|
||||
if [ -d "$etc_dir" ]; then
|
||||
echo "The /etc/ directory exists."
|
||||
else
|
||||
echo "The /etc/ directory does not exist. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# trick the gl_tertf file into moving stuff to the void
|
||||
GASLIGHT () { # good job lil dude you're doing so well
|
||||
local file="/etc/init.d/gl_tertf"
|
||||
ln -sf /dev/null "$file"
|
||||
}
|
||||
|
||||
CHECKDIR_TMP
|
||||
CHECKDIR_ETC
|
||||
GASLIGHT
|
||||
|
||||
# Kills process responsible for manipulating (and protecting) the /tmp/ file instance
|
||||
killall -9 gltertf
|
||||
|
||||
# shredding /tmp/tertf
|
||||
if [ -f "$tmp_file" ];then
|
||||
echo "Files found within /tmp/. Let's get to it."
|
||||
shred -v -u "$tmp_file"
|
||||
else
|
||||
echo "No file found within /tmp/tertf. No shredding to be done there."
|
||||
fi
|
||||
|
||||
# shredding /etc/tertf
|
||||
if [ -f "$etc_file" ]; then
|
||||
echo "Files found in /etc/. Let's get to it."
|
||||
shred -v -u "$etc_file" #-v provides verbose output to ease my anxious mind and -u deletes files after they are overwritten
|
||||
else
|
||||
echo "No file found within /etc/tertf. No shredding to be done there."
|
||||
fi
|
||||
|
||||
# check if the files have been removed
|
||||
if [ ! -f "$tmp_file" ]; then
|
||||
echo "Looks like /tmp/ is clean!"
|
||||
else
|
||||
echo "Something went wrong in /tmp/."
|
||||
fi
|
||||
|
||||
if [ ! -f "$etc_file" ]; then
|
||||
echo "Looks like /etc/ is clean!"
|
||||
else
|
||||
echo "Something went wrong in /etc/."
|
||||
fi
|
||||
|
||||
exit 0
|
39
files/lib/blue-merle/patches/index.html.patch
Normal file
39
files/lib/blue-merle/patches/index.html.patch
Normal file
@ -0,0 +1,39 @@
|
||||
--- orig/index.html
|
||||
+++ patch/index.html
|
||||
@@ -6,7 +6,7 @@
|
||||
<span class="circle active"></span>
|
||||
<span>{{t($lang.setting.btnSetting)}}</span>
|
||||
</h4>
|
||||
- <gl-btn type="purple" class="pull-right moblieBtnLate" @click="checkType" :disabled="btnStatus">{{t($lang.button.apply)}}</gl-btn>
|
||||
+ <gl-btn type="purple" class="pull-right moblieBtnLate" @click="checkType" :disabled="true">{{t($lang.button.apply)}}</gl-btn>
|
||||
</div>
|
||||
<div class="panel-body panel-status">
|
||||
<span class="setting-box clearfix list-group">
|
||||
@@ -64,6 +64,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+
|
||||
+ <div v-show="parstatus=='sim'">
|
||||
+ <div style="width:47%; display:inline-block;vertical-align:top;">
|
||||
+ <div id="title_left" style=" font-weight:bold">SIM Change Start</div>
|
||||
+ <div id="content_left" class="content_switch">{{'SIM change will be initiated'}}.
|
||||
+ </div>
|
||||
+ </div>
|
||||
+ <div style="width:47%; display:inline-block;vertical-align:top">
|
||||
+ <div id="title_right" style="font-weight:bold">SIM Change Stop</div>
|
||||
+ <div id="content_right" class="content_switch">
|
||||
+ </div>
|
||||
+ </div>
|
||||
+ </div>
|
||||
+
|
||||
</div>
|
||||
<div style="display:none">
|
||||
<label class="div-help-title">{{t($lang.setting.diy)}}</label>
|
||||
@@ -84,4 +98,4 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
-</div>
|
||||
\ No newline at end of file
|
||||
+</div>
|
41
files/lib/blue-merle/patches/index.js.patch
Normal file
41
files/lib/blue-merle/patches/index.js.patch
Normal file
@ -0,0 +1,41 @@
|
||||
--- orig/index.js
|
||||
+++ patch/index.js
|
||||
@@ -6,7 +6,7 @@
|
||||
data: function data() {
|
||||
return {
|
||||
isShow: false,
|
||||
- chooseState: ["No function (default)", "WireGuard® Client Toggle (On/Off)", "OpenVPN Client Toggle (On/Off)"],
|
||||
+ chooseState: ["No function (default)", "WireGuard® Client Toggle (On/Off)", "OpenVPN Client Toggle (On/Off)", "SIM Change (Start/Stop)"],
|
||||
// chooseState4G: ["No function (default)", "WireGuard® Client Toggle (On/Off)", "OpenVPN Client Toggle (On/Off)", ],
|
||||
param: "",
|
||||
btnStatus: true,
|
||||
@@ -79,6 +79,9 @@
|
||||
case "Tor Toggle (On/Off)":
|
||||
status = "tor";
|
||||
break;
|
||||
+ case "SIM Change (Start/Stop)":
|
||||
+ status="sim";
|
||||
+ break;
|
||||
}
|
||||
return status;
|
||||
},
|
||||
@@ -108,6 +111,9 @@
|
||||
case "tor":
|
||||
_this.param = "Tor Toggle (On/Off)";
|
||||
break;
|
||||
+ case "sim":
|
||||
+ _this.param = "SIM Change (Start/Stop)";
|
||||
+ break;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -170,6 +176,9 @@
|
||||
case 'tor':
|
||||
_this.openSetting();
|
||||
break;
|
||||
+ case 'sim':
|
||||
+ _this.openSetting();
|
||||
+ break;
|
||||
}
|
||||
},
|
||||
openSetting: function openSetting() {
|
23
files/lib/blue-merle/patches/switch_queue.patch
Normal file
23
files/lib/blue-merle/patches/switch_queue.patch
Normal file
@ -0,0 +1,23 @@
|
||||
--- orig/switch_queue
|
||||
+++ patch/switch_queue
|
||||
@@ -81,6 +81,10 @@
|
||||
"tor")
|
||||
e750-mcu " Turning TOR ON"
|
||||
;;
|
||||
+ "sim")
|
||||
+# e750-mcu " Starting SIM swap"
|
||||
+ blue-merle-switch&
|
||||
+ ;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
@@ -98,6 +102,9 @@
|
||||
check_other_vpn tor
|
||||
e750-mcu " Turning TOR OFF"
|
||||
;;
|
||||
+ "sim")
|
||||
+# e750-mcu " Stopping SIM swap"
|
||||
+ ;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
38
files/lib/blue-merle/patches/switchaction.patch
Normal file
38
files/lib/blue-merle/patches/switchaction.patch
Normal file
@ -0,0 +1,38 @@
|
||||
--- orig/switchaction
|
||||
+++ patch/switchaction
|
||||
@@ -10,6 +10,15 @@
|
||||
fi
|
||||
}
|
||||
|
||||
+toggle_sim(){
|
||||
+ local action=$1
|
||||
+ if [ "$action" = "OFF" ];then
|
||||
+ sim_switch off
|
||||
+ else
|
||||
+ sim_switch on
|
||||
+ fi
|
||||
+}
|
||||
+
|
||||
check_other_vpn(){
|
||||
wg_server=$(uci get wireguard_server.@servers[0].enable)
|
||||
ov_server=$(uci get vpn_service.global.enable)
|
||||
@@ -136,6 +145,9 @@
|
||||
check_other_vpn tor
|
||||
toggle_tor ON
|
||||
;;
|
||||
+ "sim")
|
||||
+ toggle_sim ON
|
||||
+ ;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
||||
@@ -157,6 +169,9 @@
|
||||
"tor")
|
||||
toggle_tor OFF
|
||||
;;
|
||||
+ "sim")
|
||||
+ toggle_sim OFF
|
||||
+ ;;
|
||||
"*")
|
||||
;;
|
||||
esac
|
134
files/usr/bin/blue-merle
Normal file
134
files/usr/bin/blue-merle
Normal file
@ -0,0 +1,134 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /lib/blue-merle/functions.sh
|
||||
|
||||
if [ ! -c "/dev/ttyUSB3" ]; then
|
||||
echo "Error: /dev/ttyUSB3 does not exist."
|
||||
echo "Please reboot and contact the maintainer if the problem persists."
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo -n "Swap SIM card and update IMEI? (Y/n): "
|
||||
read answer
|
||||
case $answer in
|
||||
n*) answer=0;;
|
||||
N*) answer=0;;
|
||||
*) answer=1;;
|
||||
esac
|
||||
if [[ "$answer" -eq 0 ]]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
|
||||
echo "Disabling the ME from both transmitting and receiving RF signals..."
|
||||
|
||||
old_imei=$(READ_IMEI)
|
||||
old_imsi=$(READ_IMSI)
|
||||
|
||||
answer=1
|
||||
while [[ "$answer" -eq 1 ]]; do
|
||||
gl_modem AT AT+CFUN=4 | grep -q OK
|
||||
if [[ $? -eq 1 ]]; then
|
||||
echo -n "...failed. Try again? (Y/n): "
|
||||
read answer
|
||||
case $answer in
|
||||
n*) answer=0;;
|
||||
N*) answer=0;;
|
||||
*) answer=1;;
|
||||
esac
|
||||
if [[ $answer -eq 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
answer=0
|
||||
fi
|
||||
done
|
||||
|
||||
echo -n "Please now replace the SIM card and press any key to continue. "
|
||||
read answer
|
||||
|
||||
leak=0
|
||||
|
||||
until gl_modem AT AT+CFUN=0 | grep -q OK
|
||||
do
|
||||
sleep 1
|
||||
echo "CFUN=0 failed. Trying again."
|
||||
done
|
||||
|
||||
until gl_modem AT AT+CFUN=4 | grep -q OK
|
||||
do
|
||||
leak=1
|
||||
echo "CFUN=4 failed. Trying again."
|
||||
done
|
||||
|
||||
if [[ $leak -eq 1 ]]; then
|
||||
echo
|
||||
echo "WARNING: Reset took longer than expected."
|
||||
echo
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
new_imsi=$(READ_IMSI)
|
||||
|
||||
if [[ "$old_imsi" == "$new_imsi" ]]; then
|
||||
echo
|
||||
echo "WARNING: Old IMSI equals new IMSI."
|
||||
echo
|
||||
fi
|
||||
|
||||
echo -n "Would you like to set a random (r) or deterministic (d) IMEI? (R/d): "
|
||||
read answer
|
||||
case $answer in
|
||||
d*) answer=0;;
|
||||
D*) answer=0;;
|
||||
*) answer=1;;
|
||||
esac
|
||||
if [[ "$answer" -eq 1 ]]; then
|
||||
python3 /lib/blue-merle/imei_generate.py -r
|
||||
else
|
||||
python3 /lib/blue-merle/imei_generate.py -d
|
||||
fi
|
||||
|
||||
new_imei=$(READ_IMEI)
|
||||
|
||||
if [[ "$old_imei" == "$new_imei" ]]; then
|
||||
echo
|
||||
echo "WARNING: Old IMEI equals new IMEI."
|
||||
echo
|
||||
else
|
||||
mkdir -p /tmp/modem.1-1.2
|
||||
echo "$new_imei" > /tmp/modem.1-1.2/modem-imei
|
||||
fi
|
||||
|
||||
echo "You should now reset the modem or shutdown the device."
|
||||
echo "For extra privacy, you should shutdown the device and change your location."
|
||||
echo
|
||||
echo -n "Would you like to shutdown the device (s) or reset the modem (m)? (S/m): "
|
||||
read answer
|
||||
case $answer in
|
||||
m*) answer=0;;
|
||||
M*) answer=0;;
|
||||
*) answer=1;;
|
||||
esac
|
||||
if [[ "$answer" -eq 1 ]]; then
|
||||
echo {\"poweroff\": \"1\"} >/tmp/mcu_message && sleep 0.5 && killall -17 e750-mcu
|
||||
else
|
||||
echo "Resetting modem..."
|
||||
until gl_modem AT AT+QPOWD | grep -q OK
|
||||
do
|
||||
echo "Resetting modem failed. Trying again."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
time_start=$(date +%s)
|
||||
until imsi=$(gl_modem AT AT+CIMI | grep -w -E "[0-9]{6,15}")
|
||||
do
|
||||
time_now=$(date +%s)
|
||||
echo -ne "Waiting for reset to complete. ($(($time_now-$time_start))s/30s)\r"
|
||||
sleep 1
|
||||
done
|
||||
echo
|
||||
echo "FIN"
|
||||
fi
|
||||
|
138
files/usr/bin/blue-merle-switch
Executable file
138
files/usr/bin/blue-merle-switch
Executable file
@ -0,0 +1,138 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /lib/blue-merle/functions.sh
|
||||
|
||||
if [ ! -c "/dev/ttyUSB3" ]; then
|
||||
e750-mcu "Error: /dev/ttyUSB3 does not exist."
|
||||
sleep 3
|
||||
e750-mcu "Please reboot & contact maintainer if problem persists."
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
if [ ! -f "/tmp/sim_change_start" ]; then
|
||||
echo 0 > /tmp/sim_change_start
|
||||
fi
|
||||
|
||||
if [ ! -f "/tmp/sim_change_switch" ]; then
|
||||
sim_switch off
|
||||
fi
|
||||
|
||||
now=$(date +%s)
|
||||
sim_change_last=`cat /tmp/sim_change_start`
|
||||
sim_change_diff=$((now-sim_change_last))
|
||||
|
||||
if [[ "$sim_change_diff" -lt 60 ]]; then
|
||||
e750-mcu "Please wait >1min between two SIM swaps. ($sim_change_diff s)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "$now" > /tmp/sim_change_start
|
||||
|
||||
e750-mcu "Starting SIM swap."
|
||||
sleep 3
|
||||
|
||||
i=5
|
||||
until [[ $i -lt 0 ]]
|
||||
do
|
||||
e750-mcu "Pull switch to abort ($i). "
|
||||
i=$((i-1))
|
||||
sleep 1
|
||||
|
||||
CHECK_ABORT
|
||||
done
|
||||
|
||||
e750-mcu "Continuing ..."
|
||||
sleep 1
|
||||
|
||||
e750-mcu "Disabling the MEfrom transmit- ting and recei- ving RF signals."
|
||||
sleep 3
|
||||
|
||||
old_imei=$(READ_IMEI)
|
||||
old_imsi=$(READ_IMSI)
|
||||
|
||||
CHECK_ABORT
|
||||
|
||||
answer=1
|
||||
while [[ "$answer" -eq 1 ]]; do
|
||||
gl_modem AT AT+CFUN=4 | grep -q OK
|
||||
if [[ $? -eq 1 ]]; then
|
||||
e750-mcu "Disabling failed. Trying again."
|
||||
CHECK_ABORT
|
||||
else
|
||||
answer=0
|
||||
e750-mcu "Disabled."
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
e750-mcu "Replace the SIM card. Then pull the switch."
|
||||
|
||||
while [[ `cat /tmp/sim_change_switch` = "on" ]]; do
|
||||
e750-mcu "Replace the SIM card. Then pull the switch."
|
||||
sleep 3
|
||||
done
|
||||
|
||||
e750-mcu "Switch pulled. Continuing..."
|
||||
sleep 1
|
||||
sim_switch on
|
||||
|
||||
leak=0
|
||||
|
||||
until gl_modem AT AT+CFUN=0 | grep -q OK
|
||||
do
|
||||
e750-mcu "CFUN=0 failed. Trying again."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
until gl_modem AT AT+CFUN=4 | grep -q OK
|
||||
do
|
||||
leak=1
|
||||
e750-mcu "CFUN=4 failed. Trying again."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ $leak -eq 1 ]]; then
|
||||
e750-mcu "WARNING: Reset took longer than expected."
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
|
||||
new_imsi=$(READ_IMSI)
|
||||
|
||||
if [[ "$old_imsi" == "$new_imsi" ]]; then
|
||||
e750-mcu "WARNING: Old IMSI equals new IMSI."
|
||||
sleep 3
|
||||
fi
|
||||
|
||||
e750-mcu "Setting random IMEI"
|
||||
python3 /lib/blue-merle/imei_generate.py -r
|
||||
|
||||
new_imei=$(READ_IMEI)
|
||||
|
||||
if [[ "$old_imei" == "$new_imei" ]]; then
|
||||
e750-mcu "WARNING: Old IMEI equals new IMEI."
|
||||
sleep 3
|
||||
else
|
||||
mkdir -p /tmp/modem.1-1.2
|
||||
echo "$new_imei" > /tmp/modem.1-1.2/modem-imei
|
||||
fi
|
||||
|
||||
|
||||
e750-mcu "The device will shutdown now."
|
||||
sleep 3
|
||||
e750-mcu "You should change your location before booting again."
|
||||
sleep 5
|
||||
|
||||
|
||||
i=5
|
||||
until [[ $i -eq 0 ]]
|
||||
do
|
||||
i=$((i-1))
|
||||
e750-mcu "Shutting down... ($i)"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo {\"poweroff\": \"1\"} >/tmp/mcu_message && sleep 0.5 && killall -17 e750-mcu
|
||||
|
||||
exit 0
|
7
files/usr/bin/sim_switch
Executable file
7
files/usr/bin/sim_switch
Executable file
@ -0,0 +1,7 @@
|
||||
if [ "$1" = "on" ];then
|
||||
echo "on" > /tmp/sim_change_switch
|
||||
elif [ "$1" = "off" ];then
|
||||
echo "off" > /tmp/sim_change_switch
|
||||
else
|
||||
echo "off" > /tmp/sim_change_switch
|
||||
fi
|
Loading…
Reference in New Issue
Block a user