Initial commit
This commit is contained in:
Executable
+513
@@ -0,0 +1,513 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
CONFIG_FILE="${SCRIPT_DIR}/config"
|
||||
DATA_DIR="${SCRIPT_DIR}/data"
|
||||
CLIENT_DIR="${SCRIPT_DIR}/clients"
|
||||
REGISTRY="${DATA_DIR}/clients.json"
|
||||
|
||||
die() {
|
||||
printf "ERROR: %s\n" "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
info() {
|
||||
printf "INFO: %s\n" "$*"
|
||||
}
|
||||
|
||||
require_binary() {
|
||||
command -v "$1" >/dev/null 2>&1 \
|
||||
|| die "$1 not installed"
|
||||
}
|
||||
|
||||
load_config() {
|
||||
[[ -f "$CONFIG_FILE" ]] || die "Config not found"
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG_FILE"
|
||||
}
|
||||
|
||||
init_storage() {
|
||||
|
||||
mkdir -p "$DATA_DIR"
|
||||
mkdir -p "$CLIENT_DIR"
|
||||
|
||||
if [[ ! -f "$REGISTRY" ]]; then
|
||||
echo "[]" > "$REGISTRY"
|
||||
fi
|
||||
|
||||
# Проверка валидности JSON
|
||||
jq empty "$REGISTRY" \
|
||||
|| die "Registry JSON is invalid"
|
||||
}
|
||||
|
||||
sanitize() {
|
||||
local v="$1"
|
||||
v="${v//[^a-zA-Z0-9._-]/_}"
|
||||
printf "%s" "$v"
|
||||
}
|
||||
|
||||
get_next_id() {
|
||||
|
||||
local id
|
||||
|
||||
id="$(jq -r '
|
||||
if length == 0 then 1
|
||||
else (map(.id) | max) + 1
|
||||
end
|
||||
' "$REGISTRY")"
|
||||
|
||||
[[ "$id" =~ ^[0-9]+$ ]] \
|
||||
|| die "Failed to calculate next ID"
|
||||
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
get_last_ip() {
|
||||
|
||||
jq -r '
|
||||
if length == 0 then empty
|
||||
else (sort_by(.id) | last.ip)
|
||||
end
|
||||
' "$REGISTRY"
|
||||
}
|
||||
|
||||
get_first_client_ip() {
|
||||
|
||||
local network="${SERVER_NETWORK}"
|
||||
|
||||
# Удаляем CIDR suffix
|
||||
network="${network%%/*}"
|
||||
|
||||
IFS='.' read -r o1 o2 o3 o4 <<< "$network"
|
||||
|
||||
# Первый клиент всегда .2
|
||||
echo "${o1}.${o2}.${o3}.2"
|
||||
}
|
||||
|
||||
increment_ip() {
|
||||
|
||||
local current_ip="$1"
|
||||
|
||||
# Если клиентов еще нет —
|
||||
# выдаем первый IP из SERVER_NETWORK
|
||||
if [[ -z "$current_ip" ]]; then
|
||||
get_first_client_ip
|
||||
return
|
||||
fi
|
||||
|
||||
IFS='.' read -r o1 o2 o3 o4 <<< "$current_ip"
|
||||
|
||||
((o4++))
|
||||
|
||||
# Защита от выхода за пределы /24
|
||||
if ((o4 >= 255)); then
|
||||
die "IP pool exhausted"
|
||||
fi
|
||||
|
||||
echo "${o1}.${o2}.${o3}.${o4}"
|
||||
}
|
||||
|
||||
generate_client_keys() {
|
||||
|
||||
CLIENT_PRIV="$(wg genkey)"
|
||||
|
||||
CLIENT_PUB="$(
|
||||
printf "%s" "$CLIENT_PRIV" | wg pubkey
|
||||
)"
|
||||
|
||||
CLIENT_PSK="$(wg genpsk)"
|
||||
}
|
||||
|
||||
create_client_config() {
|
||||
|
||||
local id="$1"
|
||||
local name="$2"
|
||||
local ip="$3"
|
||||
|
||||
local filename="${CLIENT_DIR}/${name}.conf"
|
||||
|
||||
cat > "$filename" <<EOF
|
||||
# Client: $name
|
||||
# ID: $id
|
||||
|
||||
[Interface]
|
||||
PrivateKey = $CLIENT_PRIV
|
||||
Address = ${ip}/32
|
||||
DNS = $DNS_SERVER
|
||||
MTU = $SERVER_MTU
|
||||
|
||||
[Peer]
|
||||
PublicKey = $SERVER_PUBLIC_KEY
|
||||
PresharedKey = $CLIENT_PSK
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
Endpoint = ${SERVER_PUBLIC_IP}:${SERVER_PUBLIC_PORT}
|
||||
PersistentKeepalive = 25
|
||||
EOF
|
||||
|
||||
qrencode \
|
||||
-s 8 \
|
||||
-o "${CLIENT_DIR}/${name}.png" \
|
||||
< "$filename"
|
||||
|
||||
info "Client config created: ${name}.conf"
|
||||
}
|
||||
|
||||
registry_add() {
|
||||
|
||||
local id="$1"
|
||||
local name="$2"
|
||||
local ip="$3"
|
||||
local c_status
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
c_status="ACTIVE"
|
||||
echo "cstatus is: "$c_status""
|
||||
jq \
|
||||
--argjson id "$id" \
|
||||
--arg name "$name" \
|
||||
--arg ip "$ip" \
|
||||
--arg pub "$CLIENT_PUB" \
|
||||
--arg priv "$CLIENT_PRIV" \
|
||||
--arg psk "$CLIENT_PSK" \
|
||||
--arg status "$c_status" \
|
||||
'
|
||||
. + [{
|
||||
id: $id,
|
||||
name: $name,
|
||||
ip: $ip,
|
||||
public_key: $pub,
|
||||
private_key: $priv,
|
||||
psk_key: $psk,
|
||||
is_enabled: $status,
|
||||
created_at: now|floor
|
||||
}]
|
||||
' "$REGISTRY" > "$tmp_file" \
|
||||
|| die "Failed to update registry"
|
||||
|
||||
mv "$tmp_file" "$REGISTRY"
|
||||
|
||||
# Проверка после записи
|
||||
jq empty "$REGISTRY" \
|
||||
|| die "Registry corrupted after write"
|
||||
}
|
||||
|
||||
create_client() {
|
||||
|
||||
local raw_name=""
|
||||
local apply_ros=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--apply_ros)
|
||||
apply_ros=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$raw_name" ]]; then
|
||||
raw_name="$1"
|
||||
else
|
||||
die "Unexpected argument: $1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$raw_name" ]] \
|
||||
|| die "Client name required"
|
||||
|
||||
local name
|
||||
name="$(sanitize "$raw_name")"
|
||||
|
||||
[[ -n "$name" ]] \
|
||||
|| die "Sanitized name is empty"
|
||||
|
||||
local id
|
||||
id="$(get_next_id)"
|
||||
|
||||
local last_ip
|
||||
last_ip="$(get_last_ip)"
|
||||
|
||||
local next_ip
|
||||
next_ip="$(increment_ip "$last_ip")"
|
||||
|
||||
generate_client_keys
|
||||
|
||||
create_client_config "$id" "$name" "$next_ip"
|
||||
|
||||
registry_add "$id" "$name" "$next_ip"
|
||||
|
||||
info "Client registered: id=$id name=$name ip=$next_ip"
|
||||
|
||||
print_mikrotik_commands
|
||||
|
||||
info "RouterOS Configuration Script has been Created: id=$id name=$name"
|
||||
|
||||
if [[ "$apply_ros" == true ]]; then
|
||||
apply_mikrotik_commands
|
||||
info "RouterOS WG Peer has been Configured: id=$id name=$name"
|
||||
else
|
||||
info "RouterOS WG Peer was not Configured (use --apply_ros to apply configuration)"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
list_clients() {
|
||||
|
||||
jq -r '
|
||||
if length == 0 then
|
||||
"No clients"
|
||||
else
|
||||
.[] | "ID=\(.id) NAME=\(.name) IP=\(.ip) STATUS=\(.is_enabled)"
|
||||
end
|
||||
' "$REGISTRY"
|
||||
}
|
||||
|
||||
disable_client() {
|
||||
|
||||
local id="${1:-}"
|
||||
|
||||
[[ "$id" =~ ^[0-9]+$ ]] \
|
||||
|| die "Invalid ID"
|
||||
|
||||
local current_status name
|
||||
|
||||
read -r current_status name < <(jq -r --argjson id "$id" '.[] | select(.id == $id) | "\(.is_enabled) \(.name)"' "$REGISTRY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$name" ]]; then
|
||||
die "Client with ID $id not found"
|
||||
fi
|
||||
|
||||
local new_status
|
||||
if [[ "$current_status" == "ACTIVE" ]]; then
|
||||
new_status="DISABLED"
|
||||
else
|
||||
new_status="ACTIVE"
|
||||
fi
|
||||
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
|
||||
jq \
|
||||
--argjson id "$id" \
|
||||
--arg new_status "$new_status" \
|
||||
'
|
||||
map(
|
||||
if .id == $id then
|
||||
.is_enabled = $new_status
|
||||
else
|
||||
.
|
||||
end
|
||||
)
|
||||
' "$REGISTRY" > "$tmp_file" \
|
||||
|| { rm -f "$tmp_file"; die "Failed to update registry status"; }
|
||||
|
||||
mv "$tmp_file" "$REGISTRY"
|
||||
|
||||
info "Client $id ($name) status changed from $current_status to $new_status"
|
||||
|
||||
}
|
||||
|
||||
enable_client() {
|
||||
|
||||
local id="${1:-}"
|
||||
|
||||
[[ "$id" =~ ^[0-9]+$ ]] \
|
||||
|| die "Invalid ID"
|
||||
|
||||
local current_status name
|
||||
|
||||
read -r current_status name < <(jq -r --argjson id "$id" '.[] | select(.id == $id) | "\(.is_enabled) \(.name)"' "$REGISTRY" 2>/dev/null)
|
||||
|
||||
if [[ -z "$name" ]]; then
|
||||
die "Client with ID $id not found"
|
||||
fi
|
||||
|
||||
local new_status
|
||||
if [[ "$current_status" == "DISABLED" ]]; then
|
||||
new_status="ACTIVE"
|
||||
else
|
||||
new_status="DISABLED"
|
||||
fi
|
||||
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
|
||||
jq \
|
||||
--argjson id "$id" \
|
||||
--arg new_status "$new_status" \
|
||||
'
|
||||
map(
|
||||
if .id == $id then
|
||||
.is_enabled = $new_status
|
||||
else
|
||||
.
|
||||
end
|
||||
)
|
||||
' "$REGISTRY" > "$tmp_file" \
|
||||
|| { rm -f "$tmp_file"; die "Failed to update registry status"; }
|
||||
|
||||
mv "$tmp_file" "$REGISTRY"
|
||||
|
||||
info "Client $id ($name) status changed from $current_status to $new_status"
|
||||
|
||||
}
|
||||
|
||||
delete_client() {
|
||||
local id="${1:-}"
|
||||
|
||||
[[ "$id" =~ ^[0-9]+$ ]] \
|
||||
|| die "Invalid ID"
|
||||
|
||||
local name
|
||||
name="$(jq -r --argjson id "$id" '.[] | select(.id == $id) | .name' "$REGISTRY" 2>/dev/null)"
|
||||
|
||||
if [[ -z "$name" ]]; then
|
||||
die "Client with ID $id not found"
|
||||
fi
|
||||
|
||||
local tmp_file
|
||||
tmp_file="$(mktemp)"
|
||||
|
||||
jq \
|
||||
--argjson id "$id" \
|
||||
'map(select(.id != $id))' \
|
||||
"$REGISTRY" > "$tmp_file" \
|
||||
|| { rm -f "$tmp_file"; die "Failed to update registry"; }
|
||||
|
||||
mv "$tmp_file" "$REGISTRY"
|
||||
|
||||
if [[ -n "$name" ]]; then
|
||||
rm -f "${CLIENT_DIR}/${name}.conf"
|
||||
rm -f "${CLIENT_DIR}/${name}.png"
|
||||
rm -f "${CLIENT_DIR}/${name}.rsc"
|
||||
fi
|
||||
|
||||
info "Client $id ($name) removed"
|
||||
}
|
||||
|
||||
print_mikrotik_commands() {
|
||||
client_ip=$(jq -r --arg val "$name" '.[] | select(.name == $val) | .ip' "$REGISTRY")
|
||||
|
||||
local mt_filename="${CLIENT_DIR}/${name}.rsc"
|
||||
|
||||
cat > "$mt_filename" <<EOF
|
||||
# RouterOS Config for $name
|
||||
/interface wireguard peers
|
||||
add interface="$SERVER_INTERFACE" \\
|
||||
public-key="$CLIENT_PUB" \\
|
||||
allowed-address="$client_ip/32" \\
|
||||
preshared-key="$CLIENT_PSK" \\
|
||||
name="$name" \\
|
||||
comment="$name"
|
||||
EOF
|
||||
}
|
||||
|
||||
test_mikrotik_commands() {
|
||||
local HOST="$MT_HOST"
|
||||
local USER="$MT_USER"
|
||||
local PASS="$MT_PASSW"
|
||||
|
||||
sshpass -p "$PASS" ssh \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
"$USER@$HOST" <<'EOF'
|
||||
echo "start"
|
||||
/sys identity print
|
||||
/sys routerboard print
|
||||
echo "done"
|
||||
EOF
|
||||
}
|
||||
|
||||
apply_mikrotik_commands() {
|
||||
local HOST="$MT_HOST"
|
||||
local USER="$MT_USER"
|
||||
local PASS="$MT_PASSW"
|
||||
LOCAL_FILE="${CLIENT_DIR}/${name}.rsc"
|
||||
REMOTE_FILE="${name}.rsc"
|
||||
# Upload file via SSH
|
||||
sshpass -p "$PASS" scp \
|
||||
-O \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o PubkeyAuthentication=no \
|
||||
"$LOCAL_FILE" "$USER@$HOST:$REMOTE_FILE"
|
||||
# Execute file remotely
|
||||
sshpass -p "$PASS" ssh \
|
||||
-T \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o PubkeyAuthentication=no \
|
||||
"$USER@$HOST" \
|
||||
"/import file-name=$REMOTE_FILE"
|
||||
# Cleanup file remotely
|
||||
sshpass -p "$PASS" ssh \
|
||||
-T \
|
||||
-o StrictHostKeyChecking=no \
|
||||
-o UserKnownHostsFile=/dev/null \
|
||||
-o PubkeyAuthentication=no \
|
||||
"$USER@$HOST" \
|
||||
"/file/remove $REMOTE_FILE"
|
||||
}
|
||||
|
||||
main() {
|
||||
|
||||
require_binary wg
|
||||
require_binary jq
|
||||
require_binary qrencode
|
||||
|
||||
load_config
|
||||
init_storage
|
||||
|
||||
case "${1:-}" in
|
||||
|
||||
create)
|
||||
shift
|
||||
create_client "$@"
|
||||
;;
|
||||
|
||||
delete)
|
||||
shift
|
||||
delete_client "$@"
|
||||
;;
|
||||
|
||||
disable)
|
||||
shift
|
||||
disable_client "$@"
|
||||
;;
|
||||
|
||||
enable)
|
||||
shift
|
||||
enable_client "$@"
|
||||
;;
|
||||
|
||||
list)
|
||||
list_clients
|
||||
;;
|
||||
|
||||
test)
|
||||
test_mikrotik_commands
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Usage:
|
||||
|
||||
- create <name> <option> # create new client
|
||||
|
||||
NOTE: use --apply_ros command to configure RouterOS WG Peer
|
||||
|
||||
- delete <id> # delete existing client by ID
|
||||
- disable <id> # disable existing client by ID
|
||||
- enable <id> # enable existing client by ID
|
||||
- list # list all clients
|
||||
- test # test remote exec"
|
||||
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user