Skip to content

Instantly share code, notes, and snippets.

@lijunle
Created January 3, 2026 04:31
Show Gist options
  • Select an option

  • Save lijunle/1e8db8d6f9eecc09e05c618ec1c62495 to your computer and use it in GitHub Desktop.

Select an option

Save lijunle/1e8db8d6f9eecc09e05c618ec1c62495 to your computer and use it in GitHub Desktop.
Xray script to show and add user config
#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE="/usr/local/etc/xray/config.json"
SERVICE_NAME="xray"
# STRING MANIPULATION for HOST/REMARKS
RAW_HOSTNAME=$(hostname)
HOST="$RAW_HOSTNAME" # Change this to host's public domain
REMARKS="$RAW_HOSTNAME"
usage() {
echo "Usage:"
echo " $0 <email> # Show existing user info + QR"
echo " $0 <email> --add # Add new user"
echo " $0 <email> --add --force # Force add (overwrite existing)"
exit 1
}
if (( EUID != 0 )); then
echo "ERROR: Must run as root." >&2
exit 1
fi
if [[ $# -lt 1 ]]; then
usage
fi
EMAIL="$1"
ADD_MODE=false
FORCE=false
# Parse flags
shift
while [[ $# -gt 0 ]]; do
case "$1" in
--add) ADD_MODE=true ;;
--force) FORCE=true ;;
--help) usage ;;
*) echo "Unknown option: $1" >&2; usage ;;
esac
shift
done
if ! command -v jq >/dev/null 2>&1 || ! command -v xray >/dev/null 2>&1 || ! command -v qrencode >/dev/null 2>&1; then
echo "ERROR: jq, xray, qrencode must be installed." >&2
exit 1
fi
# FUNCTION: Update config (handles permissions, temp file, chmod/chown)
update_config() {
local email="$1" remove_existing="$2"
local uuid
uuid=$(xray uuid)
ORIGINAL_MODE=$(stat -c %a "$CONFIG_FILE")
ORIGINAL_OWNER=$(stat -c %U:%G "$CONFIG_FILE")
tmp=$(mktemp)
# Read config file once into variable
local config_content
config_content=$(cat "$CONFIG_FILE")
if [[ "$remove_existing" == true ]]; then
# Remove existing first
config_content=$(echo "$config_content" | jq --arg email "$email" '
.inbounds[0].settings.clients |= map(select(.email != $email))
')
fi
# Add new user
echo "$config_content" | jq --arg email "$email" --arg uuid "$uuid" '
.inbounds[0].settings.clients += [{
"email": $email,
"id": $uuid,
"flow": "xtls-rprx-vision",
"level": 0
}]
' > "$tmp"
mv "$tmp" "$CONFIG_FILE"
chmod $ORIGINAL_MODE "$CONFIG_FILE"
chown $ORIGINAL_OWNER "$CONFIG_FILE"
# Return the generated UUID
echo "$uuid"
}
# FUNCTION: Show user info + QR
show_user_info() {
local uuid="$1" email="$2"
local port params link
local config_content
config_content=$(cat "$CONFIG_FILE")
port=$(echo "$config_content" | jq -r '.inbounds[0].port')
params="tls=1&tfo=1&xtls=2&remarks=${REMARKS}"
link="vless://auto:${uuid}@${HOST}:${port}?${params}#${email}"
echo "=================================================="
echo " VLESS User: $email"
echo " UUID: $uuid"
echo " HOST: $HOST"
echo " REMARKS: $REMARKS"
echo "=================================================="
echo " Link:"
echo " $link"
echo "=================================================="
echo " QR:"
qrencode -t UTF8 "$link"
}
# CHECK EXISTING USER
config_content=$(cat "$CONFIG_FILE")
EXISTING_UUID=$(echo "$config_content" | jq -r --arg email "$EMAIL" '
.inbounds[0].settings.clients[]?
| select(.email == $email)
| .id
')
if [[ "$EXISTING_UUID" != "null" && -n "$EXISTING_UUID" ]]; then
# User exists
show_user_info "$EXISTING_UUID" "$EMAIL"
echo
if [[ "$ADD_MODE" == true ]]; then
if [[ "$FORCE" == true ]]; then
echo "FORCE ADDING (overwriting existing)..."
UUID=$(update_config "$EMAIL" true)
systemctl restart "$SERVICE_NAME"
show_user_info "$UUID" "$EMAIL"
else
echo "ERROR: User exists. Use --force to overwrite."
exit 1
fi
else
exit 0
fi
elif [[ "$ADD_MODE" == true ]]; then
# User doesn't exist and add mode is on
echo "Creating new user: $EMAIL"
UUID=$(update_config "$EMAIL" false)
systemctl restart "$SERVICE_NAME"
show_user_info "$UUID" "$EMAIL"
else
# User doesn't exist and add mode is off
echo "ERROR: User '$EMAIL' does not exist."
echo "Use: $0 $EMAIL --add"
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment