Skip to content

Instantly share code, notes, and snippets.

@anivaros
Last active April 4, 2025 16:35
Show Gist options
  • Select an option

  • Save anivaros/4c24e2ddb6b30c1cee391151de8dd56e to your computer and use it in GitHub Desktop.

Select an option

Save anivaros/4c24e2ddb6b30c1cee391151de8dd56e to your computer and use it in GitHub Desktop.
XKeen (Xray) outbounds from Subscription url
#!/bin/bash
SUBSCRIPTION_URL="<YOUR_SUB_URL>"
OUTBOUNDS_CONFIG="/opt/etc/xray/configs/04_outbounds.json"
url_decode() {
echo "$(printf '%b' "${1//%/\\x}")"
}
SERVERS=$(curl -s "$SUBSCRIPTION_URL" | base64 -d)
if [[ -z "$SERVERS" ]]; then
echo "Error: Failed to load subscription data." >&2
exit 1
fi
OUTBOUNDS_JSON='{
"outbounds": [
'
COUNTRY_INDEX_FILE=$(mktemp)
for RAW_SERVER in $(echo "$SERVERS" | grep -E "^vless://|^trojan://"); do
PROTOCOL=$(echo "$RAW_SERVER" | grep -oE "^[a-z]+")
SERVER=$(echo "$RAW_SERVER" | sed -E 's/^[a-z]+:\/\///')
UUID=$(echo "$SERVER" | cut -d@ -f1)
ADDRESS=$(echo "$SERVER" | cut -d@ -f2 | cut -d: -f1)
PORT=$(echo "$SERVER" | cut -d: -f2 | cut -d? -f1)
PARAMS=$(echo "$SERVER" | cut -d? -f2)
TAG_SUFFIX_RAW=$(echo "$SERVER" | grep -oE "#.+$" | cut -c2-)
TAG_SUFFIX=$(url_decode "$TAG_SUFFIX_RAW")
TYPE=$(echo "$PARAMS" | grep -oE "type=[^&]+" | cut -d= -f2)
SECURITY=$(echo "$PARAMS" | grep -oE "security=[^&]+" | cut -d= -f2)
SNI=$(echo "$PARAMS" | grep -oE "sni=[^&]+" | cut -d= -f2)
SERVICE_NAME=$(echo "$PARAMS" | grep -oE "serviceName=[^&]+" | cut -d= -f2)
FP=$(echo "$PARAMS" | grep -oE "fp=[^&]+" | cut -d= -f2)
PBK=$(echo "$PARAMS" | grep -oE "pbk=[^&]+" | cut -d= -f2)
SID=$(echo "$PARAMS" | grep -oE "sid=[a-z0-9]+" | cut -d= -f2)
MODE=$(echo "$PARAMS" | grep -oE "mode=[^&]+" | cut -d= -f2)
AUTHORITY=$(echo "$PARAMS" | grep -oE "authority=[^&]+" | cut -d= -f2)
_PATH=$(echo "$PARAMS" | grep -oE "path=[^&]+" | cut -d= -f2)
_HOST=$(echo "$PARAMS" | grep -oE "host=[^&]+" | cut -d= -f2)
if [[ -z "$UUID" || -z "$ADDRESS" || -z "$PORT" ]]; then
echo "Skipping server: Missing essential parameters (UUID, ADDRESS, PORT)"
continue
fi
COUNTRY=$(echo "$TAG_SUFFIX" | sed -E 's/^[^a-zA-Z]*//' | sed -E 's/\[.*$//' | xargs)
if [[ -z "$COUNTRY" ]]; then
echo "Skipping server: Unable to extract country from '$TAG_SUFFIX'"
continue
fi
COUNTRY_SHORT=$(echo "$COUNTRY" | awk '{print tolower($0)}')
INDEX_KEY="${COUNTRY_SHORT}-${PROTOCOL}-${TYPE}"
CURRENT_INDEX=$(grep "^${INDEX_KEY} " "$COUNTRY_INDEX_FILE" | awk '{print $2}')
if [[ -z "$CURRENT_INDEX" ]]; then
CURRENT_INDEX=0
echo "${INDEX_KEY} $CURRENT_INDEX" >> "$COUNTRY_INDEX_FILE"
else
CURRENT_INDEX=$((CURRENT_INDEX + 1))
sed -i.bak "s/^${INDEX_KEY} .*\$/${INDEX_KEY} $CURRENT_INDEX/" "$COUNTRY_INDEX_FILE"
fi
TAG="${COUNTRY_SHORT}-${PROTOCOL}-${TYPE}-${CURRENT_INDEX}-out"
# Формируем streamSettings
STREAM_SETTINGS=$(cat <<EOF | jq -c
{
"network": "$TYPE",
"security": "$SECURITY",
"realitySettings": {
"fingerprint": "$FP",
"serverName": "$SNI",
"publicKey": "$PBK",
"shortId": "$SID"
}
}
EOF
)
# Добавляем grpcSettings или wsSettings
if [[ "$TYPE" == "grpc" ]]; then
GRPC_SETTINGS=$(cat <<EOF | jq -c
{
"serviceName": "$SERVICE_NAME",
"mode": "$MODE",
"authority": "$AUTHORITY"
}
EOF
)
STREAM_SETTINGS=$(echo "$STREAM_SETTINGS" | jq --argjson grpc "$GRPC_SETTINGS" '. + {grpcSettings: $grpc}')
elif [[ "$TYPE" == "ws" ]]; then
WS_SETTINGS=$(cat <<EOF | jq -c
{
"path": "$_PATH",
"headers": {
"Host": "$_HOST"
}
}
EOF
)
STREAM_SETTINGS=$(echo "$STREAM_SETTINGS" | jq --argjson ws "$WS_SETTINGS" '. + {wsSettings: $ws}')
fi
if [[ "$PROTOCOL" == "vless" ]]; then
OUTBOUNDS_JSON+=$(cat <<EOF
{
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "$ADDRESS",
"port": $PORT,
"users": [
{
"id": "$UUID",
"encryption": "none"
}
]
}
]
},
"streamSettings": $STREAM_SETTINGS,
"tag": "$TAG"
},
EOF
)
elif [[ "$PROTOCOL" == "trojan" ]]; then
OUTBOUNDS_JSON+=$(cat <<EOF
{
"protocol": "trojan",
"settings": {
"servers": [
{
"address": "$ADDRESS",
"port": $PORT,
"password": "$UUID"
}
]
},
"streamSettings": {
"network": "$TYPE",
"tlsSettings" : {
"serverName" : "$SNI"
},
"wsSettings": {
"path": "$_PATH",
"headers": {
"Host": "$_HOST"
}
},
"security": "$SECURITY"
},
"tag": "$TAG"
},
EOF
)
fi
done
OUTBOUNDS_JSON+='
{
"protocol": "dns",
"settings": {
"address": "8.8.8.8",
"nonIPQuery": "skip"
},
"tag": "dns-out-notru"
},
{
"protocol": "dns",
"settings": {
"address": "77.88.8.8",
"nonIPQuery": "skip"
},
"tag": "dns-out-ru"
},
{
"protocol": "freedom",
"tag": "direct-out"
},
{
"tag": "block-out",
"protocol": "blackhole",
"settings": {
"response": {
"type": "http"
}
}
}
]
}'
# Форматирование JSON с отступами
OUTBOUNDS_JSON=$(echo "$OUTBOUNDS_JSON" | jq '.')
echo "$OUTBOUNDS_JSON" > "$OUTBOUNDS_CONFIG"
rm -f "$COUNTRY_INDEX_FILE"
echo "Subscription is updated and saved to $OUTBOUNDS_CONFIG."
@anivaros
Copy link
Author

anivaros commented Dec 24, 2024

Install Bash via opkg: opkg install bash
Make script executable: chmod +x generate-xkeen-outbounds.sh
Replace <YOUR_SUB_URL> with your real sub url
Call script: bash generate-xkeen-outbounds.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment