From e32c1042b4bc92a332fd93bb28d8894f2a573145 Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Fri, 17 Nov 2023 14:43:47 +0300
Subject: [PATCH 01/17] add habr com to customgeo

---
 customgeo.dat | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/customgeo.dat b/customgeo.dat
index e79f698..057c2b1 100644
--- a/customgeo.dat
+++ b/customgeo.dat
@@ -1,5 +1,5 @@
 
-�
+�
 COHERENCE-EXTRA	lenta.com
 rugldn.net
 ruyastatic.net
@@ -12,8 +12,6 @@ yandex.net
 ruozonusercontent.com
 ru	mradx.net
 ruaestatic.net
-ru
-alicdn.com
 ru!rambler.pushwoosh.com
 ruvk.com
 ru	otm-r.com
@@ -24,7 +22,8 @@ alicdn.com
 rukp.house
 rukp.kg
 rukaspersky-labs.com
-ruqq.com
+ru	yadro.com
+ruhabr.comhabrastorage.orgqq.com
 cn	gtimg.com
 cn	zhihu.com
 cngeetest.com

From 3be5fa51c8740c701ccc841c8c05eb9b55b3c729 Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Sun, 19 Nov 2023 17:49:38 +0300
Subject: [PATCH 02/17] add statistics support

---
 README.md                    | 17 +++++++++++++
 ex.sh                        | 25 +++++++++++--------
 template_config_client.jsonc | 48 ++++++++++++++++++++++++++++++++++--
 template_config_server.jsonc | 42 +++++++++++++++++++++++++++++++
 4 files changed, 120 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md
index 9d6c17f..10b0bc7 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,23 @@ Note also that for current client config, traffic to .cn, .ru, .by and .ir sites
 [here](https://github.com/EvgenyNerush/coherence-grabber) for details. This makes your server much less attention-grabbing and suspicious,
 but your connection less anonymous.
 
+### Statistics
+
+(in bytes)
+
+Client:
+```
+xray api stats --server=127.0.0.1:8080 -name "inbound>>>http>>>traffic>>>downlink"
+xray api stats --server=127.0.0.1:8080 -name "inbound>>>http>>>traffic>>>uplink"
+xray api stats --server=127.0.0.1:8080 -name "inbound>>>socks>>>traffic>>>downlink"
+xray api stats --server=127.0.0.1:8080 -name "inbound>>>socks>>>traffic>>>uplink"
+```
+Server:
+```
+xray api stats --server=127.0.0.1:8080 -name "user>>>[email]>>>traffic>>>uplink"
+xray api stats --server=127.0.0.1:8080 -name "user>>>[email]>>>traffic>>>downlink"
+```
+
 ### Other clients
 
 For Windows, MacOS or Android you can try Nekobox, v2rayNG or ? (TODO): tests and config generation for them.
diff --git a/ex.sh b/ex.sh
index ccfa055..630c859 100755
--- a/ex.sh
+++ b/ex.sh
@@ -128,7 +128,7 @@ containing only digits 0-9 and letters a-f, for instance
 Better if it is: hosted by your VPS provider,
 in the same country, it is popular,
 and have only ports 80 (http) and 443 (https) open
-(can check with `nmap -T4 hostname`)
+(can check with nmap -T4 hostname)
 (1) www.youtube.com (default)
 (2) www.microsoft.com
 (3) www.google.com
@@ -170,13 +170,18 @@ and have only ports 80 (http) and 443 (https) open
             email="love@xray.com"
             # make server config
             jsonc2json template_config_server.jsonc \
-                | jq ".inbounds[].settings.clients[0].id=\"${id}\"
-                    | .inbounds[].settings.clients[0].email=\"${email}\"
-                    | .inbounds[0].streamSettings.realitySettings.dest=\"${fake_site}:443\"
-                    | .inbounds[1].streamSettings.realitySettings.dest=\"${fake_site}:80\"
-                    | .inbounds[].streamSettings.realitySettings.serverNames=${server_names}
-                    | .inbounds[].streamSettings.realitySettings.privateKey=\"${private_key}\"
-                    | .inbounds[].streamSettings.realitySettings.shortIds=[ \"${short_id}\" ]" \
+                | jq ".inbounds[1].settings.clients[0].id=\"${id}\"
+                    | .inbounds[2].settings.clients[0].id=\"${id}\"
+                    | .inbounds[1].settings.clients[0].email=\"${email}\"
+                    | .inbounds[2].settings.clients[0].email=\"${email}\"
+                    | .inbounds[1].streamSettings.realitySettings.dest=\"${fake_site}:443\"
+                    | .inbounds[2].streamSettings.realitySettings.dest=\"${fake_site}:80\"
+                    | .inbounds[1].streamSettings.realitySettings.serverNames=${server_names}
+                    | .inbounds[2].streamSettings.realitySettings.serverNames=${server_names}
+                    | .inbounds[1].streamSettings.realitySettings.privateKey=\"${private_key}\"
+                    | .inbounds[2].streamSettings.realitySettings.privateKey=\"${private_key}\"
+                    | .inbounds[1].streamSettings.realitySettings.shortIds=[ \"${short_id}\" ]
+                    | .inbounds[2].streamSettings.realitySettings.shortIds=[ \"${short_id}\" ]" \
                 > config_server.json
             # then make the user (not root) the owner of the file
             [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" config_server.json
@@ -286,7 +291,7 @@ containing only digits 0-9 and letters a-f, for instance
         "
         cp config_server.json config_server.json.backup
         # update server config
-        cat config_server.json.backup | jq ".inbounds[0].settings.clients += [${client}] | .inbounds[0].streamSettings.realitySettings.shortIds += [\"${short_id}\"]" > config_server.json
+        cat config_server.json.backup | jq ".inbounds[1].settings.clients += [${client}] | .inbounds[1].streamSettings.realitySettings.shortIds += [\"${short_id}\"]" > config_server.json
         # then make the user (not root) an owner of a file
         [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" config_server.json
         echo -e "${green}config_client_${username}.json is written,
@@ -317,7 +322,7 @@ then
     short_id=$(jq ".outbounds[0].streamSettings.realitySettings.shortId" $config)
     cp config_server.json config_server.json.backup
     # update server config
-    cat config_server.json.backup | jq "del(.inbounds[0].settings.clients[] | select(.email == \"${username}@example.com\")) | del(.inbounds[0].streamSettings.realitySettings.shortIds[] | select(. == ${short_id}))" > config_server.json
+    cat config_server.json.backup | jq "del(.inbounds[1].settings.clients[] | select(.email == \"${username}@example.com\")) | del(.inbounds[1].streamSettings.realitySettings.shortIds[] | select(. == ${short_id}))" > config_server.json
     # then make the user (not root) an owner of a file
     [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" config_server.json
     rm config_client_${username}.json
diff --git a/template_config_client.jsonc b/template_config_client.jsonc
index c2b2a8c..5ffa325 100644
--- a/template_config_client.jsonc
+++ b/template_config_client.jsonc
@@ -7,8 +7,44 @@
     "loglevel": "warning",
     "dnsLog": false
   },
+  // Turns on traffic statistics, see https://xtls.github.io/en/config/stats.html#statsobject
+  // and https://xtls.github.io/en/config/policy.html#policyobject
+  // and special "api" tag below
+  "stats": {
+  },
+  "policy": {
+    "levels": {
+      // default level
+      "0": {
+        "statsUserUplink": true,
+        "statsUserDownlink": true
+      }
+    },
+    "system": {
+      "statsInboundUplink": true,
+      "statsInboundDownlink": true,
+      "statsOutboundUplink": true,
+      "statsOutboundDownlink": true
+    }
+  },
+  // enables API interface https://xtls.github.io/en/config/api.html#apiobject
+  "api": {
+    "tag": "api",
+    "services": [ "StatsService" ]
+  },
   // client-side inbound configuration
   "inbounds": [
+    // gRPC API inbound, used to get statistics
+    {
+      "listen": "127.0.0.1",
+      "port": 8080,
+      "protocol": "dokodemo-door",
+      "settings": {
+        "address": "127.0.0.1"
+      },
+      "tag": "api"
+    },
+    // socks proxy
     {
       "tag": "socks",
       "port": 800,
@@ -22,12 +58,13 @@
           "tls"
         ]
       },
-      // settings of inbound `protocol` (see above)
+      // settings of inbound "protocol" (see above)
       "settings": {
         "auth": "noauth",
         "udp": true
       }
     },
+    // http/https proxy
     {
       "tag": "http",
       "port": 801,
@@ -42,7 +79,7 @@
         ],
         "routeOnly": false
       },
-      // settings of inbound `protocol` (see above)
+      // settings of inbound "protocol" (see above)
       "settings": {
         "auth": "noauth",
         "udp": true
@@ -105,6 +142,13 @@
   "routing": {
     "domainStrategy": "AsIs",
     "rules": [
+      {
+        "type": "field",
+        "inboundTag": [
+          "api"
+        ],
+        "outboundTag": "api"
+      },
       {
         "type": "field",
         "domain": [
diff --git a/template_config_server.jsonc b/template_config_server.jsonc
index 0d540fe..d99ecef 100644
--- a/template_config_server.jsonc
+++ b/template_config_server.jsonc
@@ -7,11 +7,43 @@
     "loglevel": "warning",
     "dnsLog": false
   },
+  // Turns on traffic statistics, see https://xtls.github.io/en/config/stats.html#statsobject
+  // and https://xtls.github.io/en/config/policy.html#policyobject
+  // and special "api" tag below
+  "stats": {
+  },
+  "policy": {
+    "levels": {
+      // default level
+      "0": {
+        "statsUserUplink": true,
+        "statsUserDownlink": true
+      }
+    },
+    "system": {
+      "statsInboundUplink": true,
+      "statsInboundDownlink": true,
+      "statsOutboundUplink": true,
+      "statsOutboundDownlink": true
+    }
+  },
+  // enables API interface https://xtls.github.io/en/config/api.html#apiobject
+  "api": {
+    "tag": "api",
+    "services": [ "StatsService" ]
+  },
   // Forward each inbound connections to corresponding `outboundTag`. If no rules match,
   // the traffic is sent out by the first outbound in `outbounds` section.
   "routing": {
     "domainStrategy": "IPIfNonMatch",
     "rules": [
+      {
+        "type": "field",
+        "inboundTag": [
+          "api"
+        ],
+        "outboundTag": "api"
+      },
       {
         "type": "field",
         "port": "80",
@@ -64,6 +96,16 @@
   },
   // server-side inbound configuration
   "inbounds": [
+    // gRPC API inbound, used to get statistics
+    {
+      "listen": "127.0.0.1",
+      "port": 8080,
+      "protocol": "dokodemo-door",
+      "settings": {
+        "address": "127.0.0.1"
+      },
+      "tag": "api"
+    },
     // main inbound, clients connect to it
     {
       "listen": "0.0.0.0",

From a3fce66e71428ea475fb368a4f3558123d946de8 Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Wed, 22 Nov 2023 15:34:41 +0300
Subject: [PATCH 03/17] make statistics pretty; add *stats reset* command

---
 ex.sh                        | 108 ++++++++++++++++++++++++++++++++++-
 template_config_client.jsonc |   2 -
 template_config_server.jsonc |   2 -
 3 files changed, 107 insertions(+), 5 deletions(-)

diff --git a/ex.sh b/ex.sh
index 630c859..2674fa9 100755
--- a/ex.sh
+++ b/ex.sh
@@ -7,7 +7,7 @@ green='\033[0;32m'
 yellow='\033[0;33m'
 normal='\033[0m'
 
-# strip lines with comments from jsonC
+# delete lines with comments from jsonC
 jsonc2json () {
     if [ ! -v $1 ]
     then
@@ -19,6 +19,60 @@ jsonc2json () {
     fi
 }
 
+# convert string with number of bytes to pretty form
+bytes2MB () {
+    if [ -v $1 ]
+    then
+        echo ""
+    else
+        bytes=$1
+        length=${#bytes}
+        if [ $length -gt 9 ]
+        then
+            head=${bytes::-9}
+            tail=${bytes: -9}
+            echo "${head}.${tail::2} GB"
+        elif [ $length -gt 6 ]
+        then
+            head=${bytes::-6}
+            tail=${bytes: -6}
+            echo "${head}.${tail::2} MB"
+        elif [ $length -gt 3 ]
+        then
+            head=${bytes::-3}
+            tail=${bytes: -3}
+            echo "${head}.${tail::2} kB"
+        else
+            echo "$bytes bytes"
+        fi
+    fi
+}
+
+# drop quotes (") at the start and at the end of a string
+strip_quotes () {
+    if [ -v $1 ] || [ ${#1} -lt 2 ]
+    then
+        echo ""
+    else
+        s=$1
+        s=${s: 1} # from 1 to the end
+        s=${s:: -1}   # from 0 to that is before the last one
+        echo $s
+    fi
+}
+
+# convert json string with statistics to pretty form;
+# (!) use with pipe | to deal with multiline strings correctly
+pretty_stats () {
+    read stats
+    if [ -v "$stats" ]
+    then
+        echo ""
+    else
+        bytes=$(echo $stats | jq ".stat.value")
+        echo "$(bytes2MB $(strip_quotes $bytes))"
+    fi
+}
 
 export PATH=$PATH:/usr/local/bin/ # for sudo user this can be not in PATH
 if command -v xray > /dev/null
@@ -329,6 +383,56 @@ then
         echo -e "${green}config_client_${username}.json is deleted,
 config_server.json is updated${normal}"
 
+elif [ $command = "stats" ]
+then
+    client_stats_proxy_down=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>proxy>>>traffic>>>downlink" 2> /dev/null)
+    server_stats_direct_down=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>direct>>>traffic>>>downlink" 2> /dev/null)
+    if [ ! -z "$client_stats_proxy_down" ] # output is not a zero string, hence script is running on a client
+    then
+        ## Client statistics ##
+        echo "Downloaded via server: $(echo $client_stats_proxy_down | pretty_stats)"
+        #
+        client_stats_proxy_up=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>proxy>>>traffic>>>uplink" 2> /dev/null)
+        echo "Uploaded via server: $(echo $client_stats_proxy_up | pretty_stats)"
+        #
+        client_stats_direct_down=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>direct>>>traffic>>>downlink" 2> /dev/null)
+        echo "Downloaded via client directly: $(echo $client_stats_direct_down | pretty_stats)"
+        #
+        client_stats_direct_up=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>direct>>>traffic>>>uplink" 2> /dev/null)
+        echo "Uploaded via client directly: $(echo $client_stats_direct_up | pretty_stats)"
+    elif [ ! -z "$server_stats_direct_down" ] # output is not a zero string, hence script is running on a server
+    then
+        ## Server statistics ##
+        echo "Downloaded in total: $(echo $server_stats_direct_down | pretty_stats)"
+        #
+        server_stats_direct_up=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>direct>>>traffic>>>uplink" 2> /dev/null)
+        echo "Uploaded in total: $(echo $server_stats_direct_up | pretty_stats)"
+        #
+        # Per user statistics
+        conf_file="config_server.json" # assuming xray is running with this config
+        qemails=$(cat $conf_file | jq ".inbounds[1].settings.clients[].email")
+        for qemail in ${qemails[@]}
+        do
+            echo ""
+            email=$(strip_quotes $qemail)
+            user_stats_down=$(xray api stats -server=127.0.0.1:8080 -name "user>>>${email}>>>traffic>>>downlink" 2> /dev/null)
+            echo "Downloaded by ${email}: $(echo $user_stats_down | pretty_stats)"
+            user_stats_up=$(xray api stats -server=127.0.0.1:8080 -name "user>>>${email}>>>traffic>>>uplink" 2> /dev/null)
+            echo "Uploaded by ${email}: $(echo $user_stats_up | pretty_stats)"
+        done
+    else
+        echo -e "${red}xray should be running to aquire or reset statistics${normal}"
+        exit 1
+    fi
+    #
+    if [ ! -v $2 ] && [ $2 = "reset" ]
+    then
+        echo ""
+        xray api statsquery -server=127.0.0.1:8080 -reset > /dev/null \
+            && echo -e "${green}statistics reset successfully${normal}" \
+            || echo -e "${red}statistics reset failed${normal}"
+    fi
+
 elif [ $command = "upgrade" ]
 then
     if bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
@@ -377,6 +481,8 @@ Here is the list of all available commands:
                     XRay and generate configs for server and client
     ${bold}add ${underl}username${normal}    add user with (any, fake) username to configs
     ${bold}del ${underl}username${normal}    delete user with given username from configs
+    ${bold}stats${normal}           print some traffic statistics
+    ${bold}stats reset${normal}     print statistics then set them to zero
     ${bold}upgrade${normal}         upgrade xray, do not touch configs
     ${bold}remove${normal}          remove xray"
 fi
diff --git a/template_config_client.jsonc b/template_config_client.jsonc
index 5ffa325..dee5af4 100644
--- a/template_config_client.jsonc
+++ b/template_config_client.jsonc
@@ -21,8 +21,6 @@
       }
     },
     "system": {
-      "statsInboundUplink": true,
-      "statsInboundDownlink": true,
       "statsOutboundUplink": true,
       "statsOutboundDownlink": true
     }
diff --git a/template_config_server.jsonc b/template_config_server.jsonc
index d99ecef..eb7c594 100644
--- a/template_config_server.jsonc
+++ b/template_config_server.jsonc
@@ -21,8 +21,6 @@
       }
     },
     "system": {
-      "statsInboundUplink": true,
-      "statsInboundDownlink": true,
       "statsOutboundUplink": true,
       "statsOutboundDownlink": true
     }

From 8f273899ec23735992581da515ad1ef1062a9a29 Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Wed, 22 Nov 2023 15:49:24 +0300
Subject: [PATCH 04/17] rm Statistics from Readme

---
 README.md | 17 -----------------
 ex.sh     |  2 +-
 2 files changed, 1 insertion(+), 18 deletions(-)

diff --git a/README.md b/README.md
index 10b0bc7..9d6c17f 100644
--- a/README.md
+++ b/README.md
@@ -42,23 +42,6 @@ Note also that for current client config, traffic to .cn, .ru, .by and .ir sites
 [here](https://github.com/EvgenyNerush/coherence-grabber) for details. This makes your server much less attention-grabbing and suspicious,
 but your connection less anonymous.
 
-### Statistics
-
-(in bytes)
-
-Client:
-```
-xray api stats --server=127.0.0.1:8080 -name "inbound>>>http>>>traffic>>>downlink"
-xray api stats --server=127.0.0.1:8080 -name "inbound>>>http>>>traffic>>>uplink"
-xray api stats --server=127.0.0.1:8080 -name "inbound>>>socks>>>traffic>>>downlink"
-xray api stats --server=127.0.0.1:8080 -name "inbound>>>socks>>>traffic>>>uplink"
-```
-Server:
-```
-xray api stats --server=127.0.0.1:8080 -name "user>>>[email]>>>traffic>>>uplink"
-xray api stats --server=127.0.0.1:8080 -name "user>>>[email]>>>traffic>>>downlink"
-```
-
 ### Other clients
 
 For Windows, MacOS or Android you can try Nekobox, v2rayNG or ? (TODO): tests and config generation for them.
diff --git a/ex.sh b/ex.sh
index 2674fa9..fd4f94e 100755
--- a/ex.sh
+++ b/ex.sh
@@ -56,7 +56,7 @@ strip_quotes () {
     else
         s=$1
         s=${s: 1} # from 1 to the end
-        s=${s:: -1}   # from 0 to that is before the last one
+        s=${s:: -1} # from 0 to that is before the last one
         echo $s
     fi
 }

From 4c553c638827f2cd30f84bac8e8ec6d1e4e6409a Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Thu, 23 Nov 2023 15:08:40 +0300
Subject: [PATCH 05/17] add fake servers: aws.amazon and discord

---
 ex.sh | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/ex.sh b/ex.sh
index fd4f94e..dad3811 100755
--- a/ex.sh
+++ b/ex.sh
@@ -179,16 +179,16 @@ containing only digits 0-9 and letters a-f, for instance
                 fi
             fi
             echo -e "${bold}Choose a fake site to mimic.${normal}
-Better if it is: hosted by your VPS provider,
-in the same country, it is popular,
-and have only ports 80 (http) and 443 (https) open
-(can check with nmap -T4 hostname)
+Better if it is popular and not blocked in your country:
 (1) www.youtube.com (default)
 (2) www.microsoft.com
 (3) www.google.com
 (4) www.bing.com
 (5) www.yahoo.com
-(6) your variant"
+(6) www.adobe.com
+(7) aws.amazon.com
+(8) discord.com
+(9) your variant"
             read number
             default_fake_site="www.youtube.com"
             if [ ! -v $number ]
@@ -206,6 +206,15 @@ and have only ports 80 (http) and 443 (https) open
                 then
                     fake_site="www.yahoo.com"
                 elif [ $number -eq 6 ]
+                then
+                    fake_site="www.adobe.com"
+                elif [ $number -eq 7 ]
+                then
+                    fake_site="aws.amazon.com"
+                elif [ $number -eq 8 ]
+                then
+                    fake_site="discord.com"
+                elif [ $number -eq 9 ]
                 then
                     echo -e "type your variant:"
                     read fake_site

From 6202fab22122ac8bf00fb4c9102fbb628939785c Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Mon, 4 Dec 2023 01:08:04 +0300
Subject: [PATCH 06/17] bring better UX; refactor

---
 README.md |  16 ++
 ex.sh     | 713 +++++++++++++++++++++++++++++++-----------------------
 2 files changed, 424 insertions(+), 305 deletions(-)

diff --git a/README.md b/README.md
index 9d6c17f..e81a66e 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,22 @@ Note also that for current client config, traffic to .cn, .ru, .by and .ir sites
 [here](https://github.com/EvgenyNerush/coherence-grabber) for details. This makes your server much less attention-grabbing and suspicious,
 but your connection less anonymous.
 
+### More deep description??
+
+Important: It is assumed that configs are stored and updated
+locally as `config_server.json`, `config_client.json` or
+`config_client_username.json` files. You should manually
+start XRay with one of configs, depending
+which role - server or client - XRay should play:
+```
+    sudo cp config_(role).json /usr/local/etc/xray/config.json
+    sudo systemctl start xray
+```
+or
+```
+    sudo xray run -c config_(role).json
+```
+
 ### Other clients
 
 For Windows, MacOS or Android you can try Nekobox, v2rayNG or ? (TODO): tests and config generation for them.
diff --git a/ex.sh b/ex.sh
index dad3811..ccf1ec1 100755
--- a/ex.sh
+++ b/ex.sh
@@ -1,5 +1,6 @@
 #!/usr/bin/env bash
 
+# stdout styles
 bold='\033[0;1m'
 underl='\033[0;4m'
 red='\033[0;31m'
@@ -7,6 +8,10 @@ green='\033[0;32m'
 yellow='\033[0;33m'
 normal='\033[0m'
 
+#################
+### Functions ###
+#################
+
 # delete lines with comments from jsonC
 jsonc2json () {
     if [ ! -v $1 ]
@@ -62,7 +67,7 @@ strip_quotes () {
 }
 
 # convert json string with statistics to pretty form;
-# (!) use with pipe | to deal with multiline strings correctly
+# use with pipe | to deal with multiline strings correctly!
 pretty_stats () {
     read stats
     if [ -v "$stats" ]
@@ -74,112 +79,78 @@ pretty_stats () {
     fi
 }
 
-export PATH=$PATH:/usr/local/bin/ # for sudo user this can be not in PATH
-if command -v xray > /dev/null
-then
-    xray_version=$(xray --version | head -n 1 | cut -c 6-10)
-    echo -e "${green}xray ${xray_version} detected${normal}"
-fi
-
-if command -v jq > /dev/null
-then
-    jq_installed=true
-    echo -e "${green}jq found${normal}"
-else
-    jq_installed=false
-    echo -e "${yellow}Warning: jq not installed but needed for operations with configs${normal}"
-fi
-
-if [ $(id -u) -eq 0 ]
-then
-    is_root=true
-    echo -e "${green}running as root${normal}"
-else
-    is_root=false
-    echo -e "${yellow}Warning: you should be root to install xray${normal}"
-fi
-
-command="help"
-if [ ! -v $1 ]
-then
-    command=$1
-fi
-
-if [ $command = "install" ]
-then
-
-    echo -e "${bold}Download and install xray?${normal} (Y/n)"
-    read answer_di
-    if [ -v $answer_di ] || [ $(echo $answer_di | cut -c 1) != "n" ]
+# check if the mandatory command exists
+check_command () {
+    cmd=$1
+    cmd_aim=$2
+    comment=$3
+    if command -v $cmd > /dev/null
     then
-        install_xray=true
-        if command -v xray > /dev/null
-        then
-            echo -e "xray ${version} detected, install anyway? (y/N)"
-            read answer_ia
-            if [ -v $answer_ia ] || ([ $(echo $answer_ia | cut -c 1) != "y" ] && [ $(echo $answer_ia | cut -c 1) != "Y" ])
-            then
-                install_xray=false
-            fi
-        fi
-        if $install_xray
-        then
-            if $is_root
-            then
-                if bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
-                then
-                    dat_dir="/usr/local/share/xray/"
-                    mkdir -p $dat_dir
-                    cp customgeo.dat ${dat_dir}
-                    echo -e "${green}xray installed${normal}"
-                else
-                    echo -e "${red}xray not installed, something goes wrong${normal}"
-                fi
-            else
-                echo -e "${red}You should be root, or run this script with sudo
-to install xray${normal}"
-                exit 1
-            fi
-        fi
+        echo -e "${green}${cmd} found${normal}"
+    else
+        echo -e "${red}${cmd} not found; ${cmd_aim}${normal}"
+        echo -e "${comment}"
+        exit 1
     fi
+}
 
-    echo -e "${bold}Generate configs?${normal} (Y/n)"
-    read answer_gc
-    if [ -v $answer_gc ] || [ $(echo $answer_gc | cut -c 1) != "n" ]
+# make directory `dir`; if it already exists, first move it to dir.backup;
+# if dir.backup already exists, first move it to dir.backup.backup; if it exists,
+# first delete it
+unsafe_mkdir () {
+    dir=$1
+    if [ -d "$dir" ]
     then
-        if ! $(command -v xray > /dev/null)
+        if [ -d "${dir}.backup" ]
         then
-            echo -e "${red}xray not installed, can't generate configs"
-            exit 1
-        fi
-        if ! $jq_installed
-        then
-            echo -e "${red}jq not installed, can't generate configs"
-            exit 1
-        else
-            echo -e "${bold}Enter IPv4 or IPv6 address of your xray server, or its domain name:${normal}"
-            read address
-            id=$(xray uuid) # generate random uuid for vless
-            keys=$(xray x25519)
-            private_key=$(echo $keys | cut -d " " -f 3)
-            public_key=$(echo $keys | cut -d " " -f 6)
-            # generate random short_id for grpc-reality
-            if command -v openssl > /dev/null
+            if [ -d "${dir}.backup.backup" ]
             then
-                short_id=$(openssl rand -hex 8)
-            else
-                echo -e "Enter a random (up to) 16-digit hex number,
-containing only digits 0-9 and letters a-f, for instance
-1234567890abcdef"
-                read short_id
-                if [ -v $short_id ]
-                then
-                    echo -e "${red}short id not set${normal}"
-                    exit 1
-                fi
+                rm -r "${dir}.backup.backup"
             fi
-            echo -e "${bold}Choose a fake site to mimic.${normal}
-Better if it is popular and not blocked in your country:
+            mv "${dir}.backup" "${dir}.backup.backup"
+            [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${dir}.backup.backup"
+        fi
+        mv "$dir" "${dir}.backup"
+        [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${dir}.backup"
+    fi
+    mkdir "$dir"
+    [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${dir}"
+}
+
+# copy file to file.backup with the same logic as in `unsafe_mkdir`
+cp_to_backup () {
+    file=$1
+    if [ -f "${file}.backup" ]
+    then
+        cp "${file}.backup" "${file}.backup.backup"
+        [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${file}.backup.backup"
+    fi
+    cp "$file" "${file}.backup"
+    [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" "${file}.backup"
+}
+
+# the main part of `./ex.sh conf` command, generates config files for server and clients
+conf () {
+    export PATH=$PATH:/usr/local/bin/ # brings xray to the path for sudo user
+    check_command xray "needed for config generation" "to install xray, try: sudo ./ex.sh install"
+    check_command jq "needed for operations with configs"
+    check_command openssl "needed for strong random numbers excluding some types of attacks"
+    #
+    echo -e "Enter IPv4 or IPv6 address of your xray server, or its domain name:"
+    read address
+    if [ -v $address ]
+    then
+        echo -e "${red}no address given${normal}"
+        exit 1
+    fi
+    id=$(xray uuid) # random uuid for VLESS
+    keys=$(xray x25519) # string "Private key: Abc... Public key: Xyz..."
+    private_key=$(echo $keys | cut -d " " -f 3) # get 3rd field of fields delimited by spaces
+    public_key=$(echo $keys | cut -d " " -f 6) # get 6th field
+    short_id=$(openssl rand -hex 8) # random short_id for REALITY
+    #
+    echo -e "Choose a fake site to mimic.
+Better if it is quite popular and not blocked in your country:
 (1) www.youtube.com (default)
 (2) www.microsoft.com
 (3) www.google.com
@@ -189,208 +160,358 @@ Better if it is popular and not blocked in your country:
 (7) aws.amazon.com
 (8) discord.com
 (9) your variant"
-            read number
-            default_fake_site="www.youtube.com"
-            if [ ! -v $number ]
+    read number
+    default_fake_site="www.youtube.com"
+    if [ -v $number ]
+    then
+        fake_site=$default_fake_site
+    else
+        if [ $number -eq 2 ]
+        then
+            fake_site="www.microsoft.com"
+        elif [ $number -eq 3 ]
+        then
+            fake_site="www.google.com"
+        elif [ $number -eq 4 ]
+        then
+            fake_site="www.bing.com"
+        elif [ $number -eq 5 ]
+        then
+            fake_site="www.yahoo.com"
+        elif [ $number -eq 6 ]
+        then
+            fake_site="www.adobe.com"
+        elif [ $number -eq 7 ]
+        then
+            fake_site="aws.amazon.com"
+        elif [ $number -eq 8 ]
+        then
+            fake_site="discord.com"
+        elif [ $number -eq 9 ]
+        then
+            echo -e "type your variant:"
+            read fake_site
+            if [ -v $fake_site ]
             then
-                if [ $number -eq 2 ]
-                then
-                    fake_site="www.microsoft.com"
-                elif [ $number -eq 3 ]
-                then
-                    fake_site="www.google.com"
-                elif [ $number -eq 4 ]
-                then
-                    fake_site="www.bing.com"
-                elif [ $number -eq 5 ]
-                then
-                    fake_site="www.yahoo.com"
-                elif [ $number -eq 6 ]
-                then
-                    fake_site="www.adobe.com"
-                elif [ $number -eq 7 ]
-                then
-                    fake_site="aws.amazon.com"
-                elif [ $number -eq 8 ]
-                then
-                    fake_site="discord.com"
-                elif [ $number -eq 9 ]
-                then
-                    echo -e "type your variant:"
-                    read fake_site
-                    if [ -v $fake_site ]
-                    then
-                        fake_site=$default_fake_site
-                    fi
-                else
-                    fake_site=$default_fake_site
-                fi
-            else
                 fake_site=$default_fake_site
             fi
-            server_names="[ \"$fake_site\" ]"
-            echo -e "${green}mimic ${fake_site}${normal}"
-            email="love@xray.com"
-            # make server config
-            jsonc2json template_config_server.jsonc \
-                | jq ".inbounds[1].settings.clients[0].id=\"${id}\"
-                    | .inbounds[2].settings.clients[0].id=\"${id}\"
-                    | .inbounds[1].settings.clients[0].email=\"${email}\"
-                    | .inbounds[2].settings.clients[0].email=\"${email}\"
-                    | .inbounds[1].streamSettings.realitySettings.dest=\"${fake_site}:443\"
-                    | .inbounds[2].streamSettings.realitySettings.dest=\"${fake_site}:80\"
-                    | .inbounds[1].streamSettings.realitySettings.serverNames=${server_names}
-                    | .inbounds[2].streamSettings.realitySettings.serverNames=${server_names}
-                    | .inbounds[1].streamSettings.realitySettings.privateKey=\"${private_key}\"
-                    | .inbounds[2].streamSettings.realitySettings.privateKey=\"${private_key}\"
-                    | .inbounds[1].streamSettings.realitySettings.shortIds=[ \"${short_id}\" ]
-                    | .inbounds[2].streamSettings.realitySettings.shortIds=[ \"${short_id}\" ]" \
-                > config_server.json
-            # then make the user (not root) the owner of the file
-            [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" config_server.json
-            vnext=" [
-                    {
-                        \"address\": \"${address}\",
-                        \"port\": 443,
-                        \"users\": [
-                            {
-                                \"id\": \"${id}\",
-                                \"email\": \"${email}\",
-                                \"encryption\": \"none\",
-                                \"flow\": \"xtls-rprx-vision\"
-                            }
-                        ]
-                    }
-                ]"
-            clientRealitySettings=" {
-                    \"fingerprint\": \"chrome\",
-                    \"serverName\": \"${fake_site}\",
-                    \"show\": false,
-                    \"publicKey\": \"${public_key}\",
-                    \"shortId\": \"${short_id}\",
-                }"
-            # make main client config
-            jsonc2json template_config_client.jsonc | jq ".outbounds |= map(if .settings.vnext then .settings.vnext=${vnext} else . end) | .outbounds |= map(if .streamSettings.realitySettings then .streamSettings.realitySettings=${clientRealitySettings} else . end)" > config_client.json
-            # then make the user (not root) an owner of a file
-            [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" config_client.json
+        else
+            fake_site=$default_fake_site
         fi
     fi
+    echo -e "${green}mimic ${fake_site}${normal}"
+    server_names="[ \"$fake_site\" ]"
+    email="love@xray.com"
+    #
+    unsafe_mkdir conf
+    ## Make server config ##
+    jsonc2json template_config_server.jsonc \
+        | jq ".inbounds[1].settings.clients[0].id=\"${id}\"
+            | .inbounds[2].settings.clients[0].id=\"${id}\"
+            | .inbounds[1].settings.clients[0].email=\"${email}\"
+            | .inbounds[2].settings.clients[0].email=\"${email}\"
+            | .inbounds[1].streamSettings.realitySettings.dest=\"${fake_site}:443\"
+            | .inbounds[2].streamSettings.realitySettings.dest=\"${fake_site}:80\"
+            | .inbounds[1].streamSettings.realitySettings.serverNames=${server_names}
+            | .inbounds[2].streamSettings.realitySettings.serverNames=${server_names}
+            | .inbounds[1].streamSettings.realitySettings.privateKey=\"${private_key}\"
+            | .inbounds[2].streamSettings.realitySettings.privateKey=\"${private_key}\"
+            | .inbounds[1].streamSettings.realitySettings.shortIds=[ \"${short_id}\" ]
+            | .inbounds[2].streamSettings.realitySettings.shortIds=[ \"${short_id}\" ]" \
+        > ./conf/config_server.json
+    # make the user (not root) the owner of the file
+    [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_server.json
+    vnext=" [
+            {
+                \"address\": \"${address}\",
+                \"port\": 443,
+                \"users\": [
+                    {
+                        \"id\": \"${id}\",
+                        \"email\": \"${email}\",
+                        \"encryption\": \"none\",
+                        \"flow\": \"xtls-rprx-vision\"
+                    }
+                ]
+            }
+        ]"
+    clientRealitySettings=" {
+            \"fingerprint\": \"chrome\",
+            \"serverName\": \"${fake_site}\",
+            \"show\": false,
+            \"publicKey\": \"${public_key}\",
+            \"shortId\": \"${short_id}\",
+        }"
+    ## Make main client config ##
+    jsonc2json template_config_client.jsonc \
+        | jq ".outbounds
+            |= map(if .settings.vnext then .settings.vnext=${vnext} else . end)
+            | .outbounds
+            |= map(if .streamSettings.realitySettings then .streamSettings.realitySettings=${clientRealitySettings} else . end)" \
+        > ./conf/config_client.json
+    # make the user (not root) an owner of a file
+    [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_client.json
+    if [ -f "./conf/config_client.json" ] && [ -f "./conf/config_server.json" ]
+    then
+        echo -e "${green}config files are generated${normal}"
+    else
+        echo -e "${red}config files are not generated${normal}"
+    fi
+}
 
-elif [ $command = "add" ]
-then
-    echo -e "${bold}add${normal}"
-    if ! $(command -v xray > /dev/null)
+# the main part of `./ex.sh add` command, adds config for given users and updates server config
+add () {
+    export PATH=$PATH:/usr/local/bin/ # brings xray to the path for sudo user
+    check_command xray "needed for config generation" "to install xray, try: sudo ./ex.sh install"
+    check_command jq "needed for operations with configs"
+    check_command openssl "needed for strong random numbers excluding some types of attacks"
+    if [ ! -f "./conf/config_client.json" ] || [ ! -f "./conf/config_server.json" ]
     then
-        echo -e "${red}xray needed but not installed${normal}"
+        echo -e "${red}server config and config for default user are needed
+but not present; to generate them, try
+    ./ex.sh conf${normal}"
         exit 1
     fi
-    if ! $jq_installed
+    if [ -v $1 ]
     then
-        echo -e "${red}jq needed but not installed${normal}"
-        exit 1
-    fi
-    if [ ! -f "config_client.json" ] || [ ! -r "config_server.json" ]
-    then
-        echo -e "${red}server config and config for default user are needed but not present,
-first generate them with ${normal}install${red} command.${normal}"
-        exit 1
-    fi
-    if [ -v $2 ]
-    then
-        echo -e "${red}username not set${normal}
+        echo -e "${red}usernames not set${normal}
 For default user, use config_client.json generated
-by ${underl}install${normal} command. Otherwise use non-void username,
+by ${underl}install${normal} command. Otherwise use non-void usernames,
 preferably of letters and digits only."
         exit 1
-    else
-        username=$2
     fi
-    configs=(config_client_*.json)
-    username_exists=false
-    for c in ${configs[@]}
+    # backup server config
+    cp_to_backup ./conf/config_server.json
+    # loop over usernames
+    for username in "$@"
     do
-        if [ -f $c ]
-        then
-            email=$(jq '.outbounds[0].settings.vnext[0].users[0].email' $c)
+        username_exists=false
+        client_emails=$(jq ".inbounds[1].settings.clients[].email" ./conf/config_server.json)
+        for email in ${client_emails[@]}
+        do
+            # convert "name@example.com" to name
             name=$(echo $email | cut -d "@" -f 1 | cut -c 2-)
             if [ $username = $name ]
             then
                 username_exists=true
             fi
-        fi
-    done
-    if $username_exists
-    then
-        echo -e "${red}username ${username} already exists, try another one${normal}"
-        exit 1
-    else
-        id=$(xray uuid) # generate random uuid for vless
-        # generate random short_id for grpc-reality
-        if command -v openssl > /dev/null
+        done
+        if $username_exists
         then
-            short_id=$(openssl rand -hex 8)
+            echo -e "${yellow}username ${username} already exists, no new config created fot it${normal}"
         else
-            echo -e "Enter a random (up to) 16-digit hex number,
-containing only digits 0-9 and letters a-f, for instance
-1234567890abcdef"
-            read short_id
-            if [ -v $short_id ]
+            id=$(xray uuid) # generate random uuid for vless
+            # generate random short_id for grpc-reality
+            short_id=$(openssl rand -hex 8)
+            # make new user config from default user config
+            ok1=$(cat ./conf/config_client.json | jq ".outbounds[0].settings.vnext[0].users[0].id=\"${id}\" | .outbounds[0].settings.vnext[0].users[0].email=\"${username}@example.com\" | .outbounds[0].streamSettings.realitySettings.shortId=\"${short_id}\"" > ./conf/config_client_${username}.json)
+            # then make the user (not root) an owner of a file
+            [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_client.json
+            # update server config
+            client="
+              {
+                \"id\": \"${id}\",
+                \"email\": \"${username}@example.com\",
+                \"flow\": \"xtls-rprx-vision\"
+              }
+            "
+            # update server config from backup
+            cp ./conf/config_server.json ./conf/tmpconfig.json
+            ok2=$(cat ./conf/tmpconfig.json | jq ".inbounds[1].settings.clients += [${client}] | .inbounds[1].streamSettings.realitySettings.shortIds += [\"${short_id}\"]" > ./conf/config_server.json)
+            rm ./conf/tmpconfig.json
+            # then make the user (not root) an owner of a file
+            [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_server.json
+            if $ok1 && $ok2
             then
-                echo -e "${red}short id not set${normal}"
-                exit 1
+                echo -e "${green}config_client_${username}.json is written, config_server.json is updated${normal}"
+            else
+                echo -e "${red}something went wrong with username ${username}${normal}"
             fi
         fi
-        # make new user config from default user config
-        cat config_client.json | jq ".outbounds[0].settings.vnext[0].users[0].id=\"${id}\" | .outbounds[0].settings.vnext[0].users[0].email=\"${username}@example.com\" | .outbounds[0].streamSettings.realitySettings.shortId=\"${short_id}\"" > config_client_${username}.json
-        # then make the user (not root) an owner of a file
-        [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" config_client.json
-        # update server config
-        client="
-          {
-            \"id\": \"${id}\",
-            \"email\": \"${username}@example.com\",
-            \"flow\": \"xtls-rprx-vision\"
-          }
-        "
-        cp config_server.json config_server.json.backup
-        # update server config
-        cat config_server.json.backup | jq ".inbounds[1].settings.clients += [${client}] | .inbounds[1].streamSettings.realitySettings.shortIds += [\"${short_id}\"]" > config_server.json
-        # then make the user (not root) an owner of a file
-        [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" config_server.json
-        echo -e "${green}config_client_${username}.json is written,
-config_server.json is updated${normal}"
+    done
+}
+
+# `./ex.sh push` command, copies config to xray's dir and restarts xray
+push () {
+    if [ $(id -u) -ne 0 ] # not root
+    then
+        echo -e "${red}you should have root privileges for that, try
+sudo ./ex.sh push${normal}"
+        exit 1
+    fi
+    echo -e "Which config to use, server/client/other? (S/c/o)"
+    read answer
+    if [ ! -v $answer ] && [ ${answer::1} = "c" ]
+    then
+        # use main client config
+        config="config_client.json"
+    elif [ ! -v $answer ] && [ ${answer::1} = "o" ]
+    then
+        # use config of some other user
+        echo -e "Which config from ./conf/ to use? (write the filename)"
+        read answer
+        config="$answer"
+    else
+        # use server config
+        config="config_server.json"
+    fi
+    if $(cp ./conf/${config} /usr/local/etc/xray/config.json && systemctl restart xray)
+    then
+        sleep 1s # gives time to xray restart
+        journalctl -u xray | tail -n 5 # message about xray start
+    else
+        echo -e "${red}can't copy config or start xray, try
+sudo xray run -c ./conf/${config}${normal}"
+    fi
+}
+
+#############
+### MAIN ####
+#############
+
+command="help" # default
+if [ ! -v $1 ]
+then
+    command=$1
+fi
+
+if [ $command = "install" ]
+then
+    if [ $(id -u) -ne 0 ] # not root
+    then
+        echo -e "${red}you should have root privileges to install xray, try
+sudo ./ex.sh install${normal}"
+        exit 1
+    fi
+    #
+    if command -v xray > /dev/null # xray already installed
+    then
+        echo -e "${yellow}xray ${version} detected, install anyway?${normal} (y/N)"
+        read answer
+        # default answer, answer not set or it's first letter is not `y` or `Y`
+        if [ -v $answer ] || ([ ${answer::1} != "y" ] && [ ${answer::1} != "Y" ])
+        then
+            exit 1
+        fi
+    fi
+    #
+    if bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
+    then
+        echo -e "${green}xray installed${normal}"
+        dat_dir="/usr/local/share/xray/"
+        mkdir -p $dat_dir
+        if cp customgeo.dat ${dat_dir}
+        then
+            echo -e "${green}customgeo.dat copied to ${dat_dir}${normal}"
+        else
+            echo -e "${red}customgeo.dat not copied to ${dat_dir}${normal}"
+            exit 1
+        fi
+    else
+        echo -e "${red}xray not installed, something goes wrong${normal}"
+        exit 1
+    fi
+    #
+    echo -e "Generate configs? (Y/n)"
+    read answer
+    if [ ! -v $answer ] && [ ${answer::1} = "n" ]
+    then
+        # config generation is not requested
+        echo -e "If you have a config file for xray, you can manually
+start xray with the following commands:
+    sudo cp yourconfig.json /usr/local/etc/xray/config.json
+    sudo systemctl start xray
+or
+    sudo xray run -c yourconfig.json"
+        exit 0
+    else
+        # config generation is requested
+        conf
+    fi
+    #
+    echo -e "Add other users? (Y/n)"
+    read answer
+    if [ -v $answer ] || [ ${answer::1} != "n" ]
+    then
+        echo -e "Enter usernames separated by spaces"
+        read usernames
+        add $usernames
+    fi
+    #
+    echo -e "Copy config to xray's dir and restart xray? (Y/n)"
+    read answer
+    if [ -v $answer ] || [ ${answer::1} != "n" ]
+    then
+        push
+    fi
+
+elif [ $command = "conf" ]
+then
+    conf
+    #
+    echo -e "Add other users? (Y/n)"
+    read answer
+    if [ -v $answer ] || [ ${answer::1} != "n" ]
+    then
+        echo -e "Enter usernames separated by spaces"
+        read usernames
+        add $usernames
+    fi
+    #
+    echo -e "Copy config to xray's dir and restart xray? (Y/n)"
+    read answer
+    if [ -v $answer ] || [ ${answer::1} != "n" ]
+    then
+        push
+    fi
+
+elif [ $command = "add" ]
+then
+    add "${@:2}"
+    #
+    echo -e "Copy config to xray's dir and restart xray? (Y/n)"
+    read answer
+    if [ -v $answer ] || [ ${answer::1} != "n" ]
+    then
+        push
     fi
 
 elif [ $command = "del" ]
 then
-    echo -e "${bold}del${normal}"
-    if ! $jq_installed
+    check_command jq "needed for operations with configs"
+    if [ ! -f "./conf/config_server.json" ]
     then
-        echo -e "${red}jq needed but not installed${normal}"
+        echo -e "${red}server config not found"
         exit 1
     fi
-    if [ -v $2 ]
-    then
-        echo -e "${red}username not set${normal}"
-        exit 1
-    else
-        username=$2
-    fi
-    config="config_client_${username}.json"
-    if [ ! -f $config ]
-    then
-        echo -e "${red}no config for user ${username}${normal}"
-        exit 1
-    fi
-    short_id=$(jq ".outbounds[0].streamSettings.realitySettings.shortId" $config)
-    cp config_server.json config_server.json.backup
-    # update server config
-    cat config_server.json.backup | jq "del(.inbounds[1].settings.clients[] | select(.email == \"${username}@example.com\")) | del(.inbounds[1].streamSettings.realitySettings.shortIds[] | select(. == ${short_id}))" > config_server.json
-    # then make the user (not root) an owner of a file
-    [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" config_server.json
-    rm config_client_${username}.json
-        echo -e "${green}config_client_${username}.json is deleted,
+    # backup server config
+    cp_to_backup ./conf/config_server.json
+    # loop over usernames
+    for username in "${@:2}"
+    do
+        config="./conf/config_client_${username}.json"
+        if [ ! -f $config ]
+        then
+            echo -e "${yellow}no config for user ${username}${normal}"
+        else
+            short_id=$(jq ".outbounds[0].streamSettings.realitySettings.shortId" $config)
+            cp ./conf/config_server.json ./conf/tmpconfig.json
+            # update server config
+            cat ./conf/tmpconfig.json | jq "del(.inbounds[1].settings.clients[] | select(.email == \"${username}@example.com\")) | del(.inbounds[1].streamSettings.realitySettings.shortIds[] | select(. == ${short_id}))" > ./conf/config_server.json
+            rm ./conf/tmpconfig.json
+            # then make the user (not root) an owner of a file
+            [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_server.json
+            rm config_client_${username}.json
+                echo -e "${green}config_client_${username}.json is deleted,
 config_server.json is updated${normal}"
+        fi
+    done
+    add
+    push
+
+elif [ $command = "push" ]
+then
+    push
 
 elif [ $command = "stats" ]
 then
@@ -454,26 +575,20 @@ then
 elif [ $command = "remove" ]
 then
     echo -e "Remove xray? (y/N)"
-    read answer_rx
-    if [ ! -v $answer_rx ] && ([ $(echo $answer_rx | cut -c 1) = "y" ] || [ $(echo $answer_rx | cut -c 1) = "Y" ])
+    read answer
+    if [ ! -v $answer ] && [ ${answer::1} = "y" ]
     then
-        echo -e "${red}Please type YES to remove${normal}"
-        read answer_y
-        if [ ! -v $answer_y ] && ([ $answer_y = "YES" ] || [ $answer_y = "yes" ])
+        if [ $(id -u) -ne 0 ] # not root
         then
-            if $is_root
-            then
-                if bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ remove --purge
-                then
-                    echo -e "${green}xray removed${normal}"
-                else
-                    echo -e "${red}xray not removed${normal}"
-                fi
-            else
-                echo -e "${red}You should be root, or run this script with sudo
-to remove xray${normal}"
-                exit 1
-            fi
+            echo -e "${red}you should have root privileges for that, try
+sudo ./ex.sh push${normal}"
+            exit 1
+        fi
+        if bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ remove --purge
+        then
+            echo -e "${green}xray removed${normal}"
+        else
+            echo -e "${red}xray not removed${normal}"
         fi
     fi
 
@@ -483,30 +598,18 @@ ${green}**** Hi, there! How to use: ****${normal}
 
     ${bold}./ex.sh ${underl}command${normal}
 
-Here is the list of all available commands:
+Here is a list of all the commands available:
 
     ${bold}help${normal}            show this message (default)
     ${bold}install${normal}         run interactive prompt, which asks to download and install
-                    XRay and generate configs for server and client
-    ${bold}add ${underl}username${normal}    add user with (any, fake) username to configs
-    ${bold}del ${underl}username${normal}    delete user with given username from configs
+                    XRay and generate configs
+    ${bold}conf${normal}            generate config files for server and clients  
+    ${bold}add ${underl}usernames${normal}   add users with (any, fake) usernames to configs
+    ${bold}del ${underl}usernames${normal}   delete users with given usernames from configs
+    ${bold}push${normal}            copy config to xray's dir and restarts xray
     ${bold}stats${normal}           print some traffic statistics
     ${bold}stats reset${normal}     print statistics then set them to zero
     ${bold}upgrade${normal}         upgrade xray, do not touch configs
     ${bold}remove${normal}          remove xray"
 fi
 
-echo -e "
-Command is done.
-
-${bold}Important:${normal} It is assumed that configs are stored and updated
-locally as config_server.json, config_client.json or
-config_client_username.json files. You should manually
-start XRay with one of configs, depending
-which role - server or client - XRay should play:
-    sudo cp config_(role).json /usr/local/etc/xray/config.json
-    sudo systemctl start xray
-or
-    sudo xray run -c config_(role).json
-"
-

From c34cc12800ffa82ee73478b6578c0e0a94ee1255 Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Tue, 5 Dec 2023 12:06:13 +0300
Subject: [PATCH 07/17] add txt custom geodata to use in Hiddify

---
 misc/customgeo4hiddify.txt | 77 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 77 insertions(+)
 create mode 100644 misc/customgeo4hiddify.txt

diff --git a/misc/customgeo4hiddify.txt b/misc/customgeo4hiddify.txt
new file mode 100644
index 0000000..1774178
--- /dev/null
+++ b/misc/customgeo4hiddify.txt
@@ -0,0 +1,77 @@
+domain:cn,
+domain:xn--fiqs8s,
+domain:xn--fiqz9s,
+domain:xn--55qx5d,
+domain:xn--io0a7i,
+domain:ru,
+domain:xn--p1ai,
+domain:by,
+domain:xn--90ais,
+domain:ir,
+domain:lenta.com,
+domain:gldn.net,
+domain:yastatic.net,
+domain:yandex.net,
+domain:mycdn.me,
+domain:avito.st,
+domain:gismeteo.st,
+domain:gismeteo.net,
+domain:ozonusercontent.com,
+domain:mradx.net,
+domain:aestatic.net,
+domain:rambler.pushwoosh.com,
+domain:vk.com,
+domain:otm-r.com,
+domain:2gis.com,
+domain:championat.com,
+domain:rus-tv.su,
+domain:kpcdn.net,
+domain:kp.house,
+domain:kp.kg,
+domain:kaspersky-labs.com,
+domain:yadro.com,
+domain:boosty.to,
+domain:habr.com,
+domain:habrastorage.org,
+domain:qq.com,
+domain:gtimg.com,
+domain:zhihu.com,
+domain:geetest.com,
+domain:zhimg.com,
+domain:126.net,
+domain:163.com,
+domain:25ku.com,
+domain:sohu.com,
+domain:bcebos.com,
+domain:ifengimg.com,
+domain:taobao.com,
+domain:bdstatic.com,
+domain:ad-survey.com,
+domain:ifeng.com,
+domain:alicdn.com,
+domain:jd.com,
+domain:aliapp.org,
+domain:aliyun.com,
+domain:iqiyi.com,
+domain:mgtv.com,
+domain:iqiyipic.com,
+domain:hdslb.com,
+domain:360buyimg.com,
+domain:bilibili.com,
+domain:alibaba.com,
+domain:xiami.com,
+domain:yinyuetai.com,
+domain:ctrip.com,
+domain:tripcdn.com,
+domain:c-ctrip.com,
+domain:fliggy.com,
+domain:qyerstatic.com,
+domain:baidu.com,
+domain:mafengwo.net,
+domain:meituan.com,
+domain:dianping.com,
+domain:bdimg.com,
+domain:chuimg.com,
+domain:autonavi.com,
+domain:amap.com,
+domain:mmstat.com,

From 19c136a860e149568fd57189f0453f8e189270c8 Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Wed, 6 Dec 2023 12:47:31 +0300
Subject: [PATCH 08/17] add link command

---
 ex.sh | 50 +++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 43 insertions(+), 7 deletions(-)

diff --git a/ex.sh b/ex.sh
index ccf1ec1..508282e 100755
--- a/ex.sh
+++ b/ex.sh
@@ -478,6 +478,11 @@ then
 
 elif [ $command = "del" ]
 then
+    if [ -v $2 ]
+    then
+        echo -e "${red}usernames not set${normal}"
+        exit 1
+    fi
     check_command jq "needed for operations with configs"
     if [ ! -f "./conf/config_server.json" ]
     then
@@ -497,22 +502,50 @@ then
             short_id=$(jq ".outbounds[0].streamSettings.realitySettings.shortId" $config)
             cp ./conf/config_server.json ./conf/tmpconfig.json
             # update server config
-            cat ./conf/tmpconfig.json | jq "del(.inbounds[1].settings.clients[] | select(.email == \"${username}@example.com\")) | del(.inbounds[1].streamSettings.realitySettings.shortIds[] | select(. == ${short_id}))" > ./conf/config_server.json
+            ok1=$(cat ./conf/tmpconfig.json | jq "del(.inbounds[1].settings.clients[] | select(.email == \"${username}@example.com\")) | del(.inbounds[1].streamSettings.realitySettings.shortIds[] | select(. == ${short_id}))" > ./conf/config_server.json)
             rm ./conf/tmpconfig.json
             # then make the user (not root) an owner of a file
             [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_server.json
-            rm config_client_${username}.json
+            ok2=$(rm ./conf/config_client_${username}.json)
+            if $ok1 && $ok2
+            then
                 echo -e "${green}config_client_${username}.json is deleted,
 config_server.json is updated${normal}"
+            else
+                echo -e "${red}something went wrong with username ${username}${normal}"
+            fi
         fi
     done
-    add
-    push
+    echo -e "Copy config to xray's dir and restart xray? (Y/n)"
+    read answer
+    if [ -v $answer ] || [ ${answer::1} != "n" ]
+    then
+        push
+    fi
 
 elif [ $command = "push" ]
 then
     push
 
+elif [ $command = "link" ]
+then
+    conf_file=$2
+    if [ -v $conf_file ]
+    then
+        echo -e "${red}no config is given${normal}"
+        exit 1
+    fi
+    id=$(strip_quotes $(jq ".outbounds[0].settings.vnext[0].users[0].id" $conf_file))
+    address=$(strip_quotes $(jq ".outbounds[0].settings.vnext[0].address" $conf_file))
+    port=$(jq ".outbounds[0].settings.vnext[0].port" $conf_file)
+    public_key=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.publicKey" $conf_file))
+    server_name=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.serverName" $conf_file))
+    short_id=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.shortId" $conf_file))
+    link="vless://${id}@${address}:${port}?fragment=&security=reality&encryption=none&pbk=${public_key}&headerType=none&fp=chrome&type=tcp&flow=xtls-rprx-vision&sni=${server_name}&sid=${short_id}#easy-xray+%F0%9F%97%BD"
+    echo -e "${yellow}don't forget to share misc/customgeo4hiddify.txt as well
+${green}here is your link:${normal}"
+    echo $link
+
 elif [ $command = "stats" ]
 then
     client_stats_proxy_down=$(xray api stats -server=127.0.0.1:8080 -name "outbound>>>proxy>>>traffic>>>downlink" 2> /dev/null)
@@ -601,12 +634,15 @@ ${green}**** Hi, there! How to use: ****${normal}
 Here is a list of all the commands available:
 
     ${bold}help${normal}            show this message (default)
-    ${bold}install${normal}         run interactive prompt, which asks to download and install
-                    XRay and generate configs
+    ${bold}install${normal}         run interactive prompt, which asks to download and
+                    install XRay, and generates configs
     ${bold}conf${normal}            generate config files for server and clients  
-    ${bold}add ${underl}usernames${normal}   add users with (any, fake) usernames to configs
+    ${bold}add ${underl}usernames${normal}   add users with given usernames to configs,
+                    usernames should by separated by spaces
     ${bold}del ${underl}usernames${normal}   delete users with given usernames from configs
     ${bold}push${normal}            copy config to xray's dir and restarts xray
+    ${bold}link ${underl}config${normal}     convert user config to a link acceptable by
+                    client applications such as Hiddify or V2ray
     ${bold}stats${normal}           print some traffic statistics
     ${bold}stats reset${normal}     print statistics then set them to zero
     ${bold}upgrade${normal}         upgrade xray, do not touch configs

From 83f2232c05f53ea813aaaedcf67cbbf061ec10ff Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Fri, 8 Dec 2023 09:47:52 +0300
Subject: [PATCH 09/17] add customgeo rules for nekoray

---
 misc/customgeo4nekoray.txt | 77 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 77 insertions(+)
 create mode 100644 misc/customgeo4nekoray.txt

diff --git a/misc/customgeo4nekoray.txt b/misc/customgeo4nekoray.txt
new file mode 100644
index 0000000..3b636c6
--- /dev/null
+++ b/misc/customgeo4nekoray.txt
@@ -0,0 +1,77 @@
+domain:cn
+domain:xn--fiqs8s
+domain:xn--fiqz9s
+domain:xn--55qx5d
+domain:xn--io0a7i
+domain:ru
+domain:xn--p1ai
+domain:by
+domain:xn--90ais
+domain:ir
+domain:lenta.com
+domain:gldn.net
+domain:yastatic.net
+domain:yandex.net
+domain:mycdn.me
+domain:avito.st
+domain:gismeteo.st
+domain:gismeteo.net
+domain:ozonusercontent.com
+domain:mradx.net
+domain:aestatic.net
+domain:rambler.pushwoosh.com
+domain:vk.com
+domain:otm-r.com
+domain:2gis.com
+domain:championat.com
+domain:rus-tv.su
+domain:kpcdn.net
+domain:kp.house
+domain:kp.kg
+domain:kaspersky-labs.com
+domain:yadro.com
+domain:boosty.to
+domain:habr.com
+domain:habrastorage.org
+domain:qq.com
+domain:gtimg.com
+domain:zhihu.com
+domain:geetest.com
+domain:zhimg.com
+domain:126.net
+domain:163.com
+domain:25ku.com
+domain:sohu.com
+domain:bcebos.com
+domain:ifengimg.com
+domain:taobao.com
+domain:bdstatic.com
+domain:ad-survey.com
+domain:ifeng.com
+domain:alicdn.com
+domain:jd.com
+domain:aliapp.org
+domain:aliyun.com
+domain:iqiyi.com
+domain:mgtv.com
+domain:iqiyipic.com
+domain:hdslb.com
+domain:360buyimg.com
+domain:bilibili.com
+domain:alibaba.com
+domain:xiami.com
+domain:yinyuetai.com
+domain:ctrip.com
+domain:tripcdn.com
+domain:c-ctrip.com
+domain:fliggy.com
+domain:qyerstatic.com
+domain:baidu.com
+domain:mafengwo.net
+domain:meituan.com
+domain:dianping.com
+domain:bdimg.com
+domain:chuimg.com
+domain:autonavi.com
+domain:amap.com
+domain:mmstat.com

From cf38df9fa465aa1af912cbc775ed42d1d7f5b907 Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Sat, 9 Dec 2023 22:51:42 +0300
Subject: [PATCH 10/17] update customgeo; add custom rules in format
 appropriate for hiddigy and nekoray

---
 customgeo.dat              | 48 ++++++++++++++++++++++++++++----------
 misc/customgeo4hiddify.txt | 18 +++++++++++---
 misc/customgeo4nekoray.txt | 18 +++++++++++---
 3 files changed, 66 insertions(+), 18 deletions(-)

diff --git a/customgeo.dat b/customgeo.dat
index 057c2b1..7cbdfa7 100644
--- a/customgeo.dat
+++ b/customgeo.dat
@@ -1,29 +1,53 @@
 
-�
-COHERENCE-EXTRA	lenta.com
+�
+COHERENCE-EXTRAhabr.comhabrastorage.org	lenta.com
 rugldn.net
 ruyastatic.net
 ru
 yandex.net
 rumycdn.me
 ruavito.st
-rugismeteo.st
-rugismeteo.net
-ruozonusercontent.com
+rugismeteo.st
+ru
+by gismeteo.net
+ru
+byozonusercontent.com
 ru	mradx.net
 ruaestatic.net
-ru!rambler.pushwoosh.com
-ruvk.com
+ru!
pushwoosh.com
+ru
+byvk.com
+ru
+byuserapi.com
 ru	otm-r.com
 ru2gis.com
 ruchampionat.com
 ru	rus-tv.su
-ru	kpcdn.net
-rukp.house
-rukp.kg
-rukaspersky-labs.com
+ru	kpcdn.net
+ru
+bykp.house
+ru
+bykp.kg
+ru
+bykaspersky-labs.com
 ru	yadro.com
-ruhabr.comhabrastorage.orgqq.com
+ru	boosty.to
+rueaglecdn.com
+rulivejournal.com
+rulivejournal.net
+ruedc.sale
+ru
+by"stellarlabs.ai
+ru
+bymyminsk.com
+by#sputniknews.com
+ru
+by$sputnikglobe.com
+ru
+bydigikala.com
+irbale.ai
+ru
+irqq.com
 cn	gtimg.com
 cn	zhihu.com
 cngeetest.com
diff --git a/misc/customgeo4hiddify.txt b/misc/customgeo4hiddify.txt
index 1774178..5151aea 100644
--- a/misc/customgeo4hiddify.txt
+++ b/misc/customgeo4hiddify.txt
@@ -1,3 +1,4 @@
+geosite:cn,
 domain:cn,
 domain:xn--fiqs8s,
 domain:xn--fiqz9s,
@@ -8,6 +9,8 @@ domain:xn--p1ai,
 domain:by,
 domain:xn--90ais,
 domain:ir,
+domain:habr.com,
+domain:habrastorage.org,
 domain:lenta.com,
 domain:gldn.net,
 domain:yastatic.net,
@@ -19,8 +22,9 @@ domain:gismeteo.net,
 domain:ozonusercontent.com,
 domain:mradx.net,
 domain:aestatic.net,
-domain:rambler.pushwoosh.com,
+domain:pushwoosh.com,
 domain:vk.com,
+domain:userapi.com,
 domain:otm-r.com,
 domain:2gis.com,
 domain:championat.com,
@@ -31,8 +35,16 @@ domain:kp.kg,
 domain:kaspersky-labs.com,
 domain:yadro.com,
 domain:boosty.to,
-domain:habr.com,
-domain:habrastorage.org,
+domain:eaglecdn.com,
+domain:livejournal.com,
+domain:livejournal.net,
+domain:edc.sale,
+domain:stellarlabs.ai,
+domain:myminsk.com,
+domain:sputniknews.com,
+domain:sputnikglobe.com,
+domain:digikala.com,
+domain:bale.ai,
 domain:qq.com,
 domain:gtimg.com,
 domain:zhihu.com,
diff --git a/misc/customgeo4nekoray.txt b/misc/customgeo4nekoray.txt
index 3b636c6..3fdab3e 100644
--- a/misc/customgeo4nekoray.txt
+++ b/misc/customgeo4nekoray.txt
@@ -1,3 +1,4 @@
+geosite:cn
 domain:cn
 domain:xn--fiqs8s
 domain:xn--fiqz9s
@@ -8,6 +9,8 @@ domain:xn--p1ai
 domain:by
 domain:xn--90ais
 domain:ir
+domain:habr.com
+domain:habrastorage.org
 domain:lenta.com
 domain:gldn.net
 domain:yastatic.net
@@ -19,8 +22,9 @@ domain:gismeteo.net
 domain:ozonusercontent.com
 domain:mradx.net
 domain:aestatic.net
-domain:rambler.pushwoosh.com
+domain:pushwoosh.com
 domain:vk.com
+domain:userapi.com
 domain:otm-r.com
 domain:2gis.com
 domain:championat.com
@@ -31,8 +35,16 @@ domain:kp.kg
 domain:kaspersky-labs.com
 domain:yadro.com
 domain:boosty.to
-domain:habr.com
-domain:habrastorage.org
+domain:eaglecdn.com
+domain:livejournal.com
+domain:livejournal.net
+domain:edc.sale
+domain:stellarlabs.ai
+domain:myminsk.com
+domain:sputniknews.com
+domain:sputnikglobe.com
+domain:digikala.com
+domain:bale.ai
 domain:qq.com
 domain:gtimg.com
 domain:zhihu.com

From 322bfca3f1b7780e3dc94f350a26af04bbacf1ae Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Sat, 9 Dec 2023 22:53:39 +0300
Subject: [PATCH 11/17] remove quic

---
 template_config_server.jsonc | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/template_config_server.jsonc b/template_config_server.jsonc
index eb7c594..d186e4f 100644
--- a/template_config_server.jsonc
+++ b/template_config_server.jsonc
@@ -157,8 +157,7 @@
         "enabled": true,
         "destOverride": [
           "http",
-          "tls",
-          "quic"
+          "tls"
         ]
       }
     },
@@ -216,8 +215,7 @@
         "enabled": true,
         "destOverride": [
           "http",
-          "tls",
-          "quic"
+          "tls"
         ]
       }
     }

From 049125967b0eba16c0f7c3d015d5cc05710d6320 Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Thu, 14 Dec 2023 13:51:01 +0300
Subject: [PATCH 12/17] add *import* command

---
 ex.sh | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++------
 1 file changed, 77 insertions(+), 8 deletions(-)

diff --git a/ex.sh b/ex.sh
index 508282e..83a5646 100755
--- a/ex.sh
+++ b/ex.sh
@@ -2,6 +2,7 @@
 
 # stdout styles
 bold='\033[0;1m'
+italic='\033[0;3m'
 underl='\033[0;4m'
 red='\033[0;31m'
 green='\033[0;32m'
@@ -256,6 +257,7 @@ Better if it is quite popular and not blocked in your country:
         echo -e "${green}config files are generated${normal}"
     else
         echo -e "${red}config files are not generated${normal}"
+        exit 1
     fi
 }
 
@@ -306,7 +308,7 @@ preferably of letters and digits only."
             # make new user config from default user config
             ok1=$(cat ./conf/config_client.json | jq ".outbounds[0].settings.vnext[0].users[0].id=\"${id}\" | .outbounds[0].settings.vnext[0].users[0].email=\"${username}@example.com\" | .outbounds[0].streamSettings.realitySettings.shortId=\"${short_id}\"" > ./conf/config_client_${username}.json)
             # then make the user (not root) an owner of a file
-            [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_client.json
+            [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ./conf/config_client_${username}.json
             # update server config
             client="
               {
@@ -315,7 +317,7 @@ preferably of letters and digits only."
                 \"flow\": \"xtls-rprx-vision\"
               }
             "
-            # update server config from backup
+            # update server config
             cp ./conf/config_server.json ./conf/tmpconfig.json
             ok2=$(cat ./conf/tmpconfig.json | jq ".inbounds[1].settings.clients += [${client}] | .inbounds[1].streamSettings.realitySettings.shortIds += [\"${short_id}\"]" > ./conf/config_server.json)
             rm ./conf/tmpconfig.json
@@ -325,7 +327,7 @@ preferably of letters and digits only."
             then
                 echo -e "${green}config_client_${username}.json is written, config_server.json is updated${normal}"
             else
-                echo -e "${red}something went wrong with username ${username}${normal}"
+                echo -e "${yellow}something went wrong with username ${username}${normal}"
             fi
         fi
     done
@@ -572,7 +574,7 @@ then
         echo "Uploaded in total: $(echo $server_stats_direct_up | pretty_stats)"
         #
         # Per user statistics
-        conf_file="config_server.json" # assuming xray is running with this config
+        conf_file="./conf/config_server.json" # assuming xray is running with this config
         qemails=$(cat $conf_file | jq ".inbounds[1].settings.clients[].email")
         for qemail in ${qemails[@]}
         do
@@ -596,6 +598,65 @@ then
             || echo -e "${red}statistics reset failed${normal}"
     fi
 
+elif [ $command = "import" ]
+then
+    if [ -v $2 ] || [ -v $3 ]
+    then
+        echo -e "${red}both directories (from and to) should be set${normal}"
+        exit 1
+    fi
+    from=$2
+    to=$3
+    # backup the server and the main client configs
+    cp_to_backup ${to}/config_server.json
+    #
+    configs="${from}/config_client_*.json"
+    for c in $configs
+    do
+        if [ -f $c ]
+        then
+            uname_with_json=$(echo $c | cut -d "_" -f 3-) # remove "config_client_"
+            uname_from_filename=${uname_with_json:: -5} # remove ".json"
+            email=$(strip_quotes $(jq ".outbounds[0].settings.vnext[0].users[0].email" $c))
+            uname_from_email=${email%@*} # remove "@example.com"
+            if [ $uname_from_filename != $uname_from_email ]
+            then
+                echo -e "${yellow}username ${uname_from_filename} (from filename) inconsistent with username ${uname_from_email} (from email),
+    continue with name from email${normal}"
+            fi
+            if [ -f ${to}/config_client_${uname_from_email}.json ] # username already exists
+            then
+                echo -e "${yellow}username ${uname_from_email} already exists in ${to}, no new config created fot it${normal}"
+            else
+                id=$(strip_quotes $(jq ".outbounds[0].settings.vnext[0].users[0].id" $c))
+                short_id=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.shortId" $c))
+                # make new user config
+                ok1=$(cat ${to}/config_client.json | jq ".outbounds[0].settings.vnext[0].users[0].id=\"${id}\" | .outbounds[0].settings.vnext[0].users[0].email=\"${uname_from_email}@example.com\" | .outbounds[0].streamSettings.realitySettings.shortId=\"${short_id}\"" > ${to}/config_client_${uname_from_email}.json)
+                # then make the user (not root) an owner of a file
+                [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ${to}/config_client_${uname_from_email}.json
+                # update server config
+                client="
+                  {
+                    \"id\": \"${id}\",
+                    \"email\": \"${uname_from_email}@example.com\",
+                    \"flow\": \"xtls-rprx-vision\"
+                  }
+                "
+                cp ${to}/config_server.json ${to}/tmpconfig.json
+                ok2=$(cat ${to}/tmpconfig.json | jq ".inbounds[1].settings.clients += [${client}] | .inbounds[1].streamSettings.realitySettings.shortIds += [\"${short_id}\"]" > ${to}/config_server.json)
+                rm ${to}/tmpconfig.json
+                # then make the user (not root) an owner of a file
+                [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ${to}/config_server.json
+                if $ok1 && $ok2
+                then
+                    echo -e "${green}${to}/config_client_${uname_from_email}.json is written, config_server.json is updated${normal}"
+                else
+                    echo -e "${yellow}something went wrong with username ${uname_from_email}${normal}"
+                fi
+            fi
+        fi
+    done
+
 elif [ $command = "upgrade" ]
 then
     if bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
@@ -627,24 +688,32 @@ sudo ./ex.sh push${normal}"
 
 else # "help", default
     echo -e "
-${green}**** Hi, there! How to use: ****${normal}
+${green}**** Hi, there! ****${normal}
+
+The aim of ${italic}easy-xray${normal} is to help to administrate an xray server.
+It's all about ${italic}conf${normal} directory that contains the server config
+${italic}config_server.json${normal}, the main client config ${italic}config_client.json${normal} and
+configs for other users ${italic}config_client_*.json${normal}. How to use it:
 
     ${bold}./ex.sh ${underl}command${normal}
 
 Here is a list of all the commands available:
 
     ${bold}help${normal}            show this message (default)
-    ${bold}install${normal}         run interactive prompt, which asks to download and
-                    install XRay, and generates configs
+    ${bold}install${normal}         run interactive prompt, that asks to download and
+                    install XRay, and to generate configs
     ${bold}conf${normal}            generate config files for server and clients  
     ${bold}add ${underl}usernames${normal}   add users with given usernames to configs,
                     usernames should by separated by spaces
     ${bold}del ${underl}usernames${normal}   delete users with given usernames from configs
-    ${bold}push${normal}            copy config to xray's dir and restarts xray
+    ${bold}push${normal}            copy config to xray's dir and restart xray
     ${bold}link ${underl}config${normal}     convert user config to a link acceptable by
                     client applications such as Hiddify or V2ray
     ${bold}stats${normal}           print some traffic statistics
     ${bold}stats reset${normal}     print statistics then set them to zero
+    ${bold}import ${underl}from${normal} ${underl}to${normal}  import users from one directory that contains
+                    user configs to another directory that contains
+                    server config and the main client config
     ${bold}upgrade${normal}         upgrade xray, do not touch configs
     ${bold}remove${normal}          remove xray"
 fi

From 3b1b8d596d756145b70dc9ff7dcc2de3cc47bfbb Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Thu, 14 Dec 2023 17:24:26 +0300
Subject: [PATCH 13/17] fix for Tor

---
 template_config_client.jsonc | 5 +++--
 template_config_server.jsonc | 6 ++++--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/template_config_client.jsonc b/template_config_client.jsonc
index dee5af4..d9f1f24 100644
--- a/template_config_client.jsonc
+++ b/template_config_client.jsonc
@@ -54,7 +54,8 @@
         "destOverride": [
           "http",
           "tls"
-        ]
+        ],
+        "routeOnly": true
       },
       // settings of inbound "protocol" (see above)
       "settings": {
@@ -75,7 +76,7 @@
           "http",
           "tls"
         ],
-        "routeOnly": false
+        "routeOnly": true
       },
       // settings of inbound "protocol" (see above)
       "settings": {
diff --git a/template_config_server.jsonc b/template_config_server.jsonc
index d186e4f..8fb66be 100644
--- a/template_config_server.jsonc
+++ b/template_config_server.jsonc
@@ -158,7 +158,8 @@
         "destOverride": [
           "http",
           "tls"
-        ]
+        ],
+        "routeOnly": true
       }
     },
     // extra inbound; its main purpose is to get fallback to "dest" at port 80. Many regular websites
@@ -216,7 +217,8 @@
         "destOverride": [
           "http",
           "tls"
-        ]
+        ],
+        "routeOnly": true
       }
     }
   ],

From da8047ec6d44b6498ae8ec15c1a337439552252d Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Fri, 15 Dec 2023 15:56:59 +0300
Subject: [PATCH 14/17] update Readmy, add instructions for client apps

---
 .gitignore                      |   3 +-
 HiddifyNG.en.md                 |  14 ++++
 Nekoray.ru.md                   |  16 ++++
 README.md                       | 127 ++++++++++++++++++++++----------
 V2RayNG.ru.md                   |  11 +++
 browser-proxy-settings.png      | Bin 69941 -> 0 bytes
 ex.sh                           |   4 +-
 figs/browser-proxy-settings.png | Bin 0 -> 81012 bytes
 figs/hiddify-1.jpg              | Bin 0 -> 31089 bytes
 figs/hiddify-2.jpg              | Bin 0 -> 28826 bytes
 figs/hiddify-3.jpg              | Bin 0 -> 43977 bytes
 figs/hiddify-4.jpg              | Bin 0 -> 75876 bytes
 figs/nekoray-1.jpg              | Bin 0 -> 75350 bytes
 figs/nekoray-2.jpg              | Bin 0 -> 70596 bytes
 figs/nekoray-3.jpg              | Bin 0 -> 160578 bytes
 figs/nekoray-5.jpg              | Bin 0 -> 52038 bytes
 figs/nekoray-6.jpg              | Bin 0 -> 139613 bytes
 figs/v2ray-1.jpg                | Bin 0 -> 19384 bytes
 figs/v2ray-2.jpg                | Bin 0 -> 63295 bytes
 figs/v2ray-3.jpg                | Bin 0 -> 87211 bytes
 figs/xray-schematic.png         | Bin 0 -> 19728 bytes
 21 files changed, 131 insertions(+), 44 deletions(-)
 create mode 100644 HiddifyNG.en.md
 create mode 100644 Nekoray.ru.md
 create mode 100644 V2RayNG.ru.md
 delete mode 100644 browser-proxy-settings.png
 create mode 100644 figs/browser-proxy-settings.png
 create mode 100644 figs/hiddify-1.jpg
 create mode 100644 figs/hiddify-2.jpg
 create mode 100644 figs/hiddify-3.jpg
 create mode 100644 figs/hiddify-4.jpg
 create mode 100644 figs/nekoray-1.jpg
 create mode 100644 figs/nekoray-2.jpg
 create mode 100644 figs/nekoray-3.jpg
 create mode 100644 figs/nekoray-5.jpg
 create mode 100644 figs/nekoray-6.jpg
 create mode 100644 figs/v2ray-1.jpg
 create mode 100644 figs/v2ray-2.jpg
 create mode 100644 figs/v2ray-3.jpg
 create mode 100644 figs/xray-schematic.png

diff --git a/.gitignore b/.gitignore
index 41428ba..5fd76d2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1 @@
-config_*.json
-config_*.json.backup
+figs/*.kra
diff --git a/HiddifyNG.en.md b/HiddifyNG.en.md
new file mode 100644
index 0000000..cab1bd4
--- /dev/null
+++ b/HiddifyNG.en.md
@@ -0,0 +1,14 @@
+[HiddifyNG in Google Play](https://play.google.com/store/apps/details?id=ang.hiddify.com&pcampaignid=web_share)
+
+Copy config in a form of a link. Open Hiddify app, click Clipboard button, enter any name for a group (e.g. xray), press Confirm. Then click
+Configs (on the bottom), you should see "easy-xray" with your server ip. Go to Settings, Custom rules, direct URL or IP section, and paste
+the content of `misc/customgeo4hiddify.txt` as plain text. Save, then click to connect/start.
+
+![](figs/hiddify-1.jpg)
+
+![](figs/hiddify-2.jpg)
+
+![](figs/hiddify-3.jpg)
+
+![](figs/hiddify-4.jpg)
+
diff --git a/Nekoray.ru.md b/Nekoray.ru.md
new file mode 100644
index 0000000..7b2d4bf
--- /dev/null
+++ b/Nekoray.ru.md
@@ -0,0 +1,16 @@
+[Релизы Nekoray (см. Assets)](https://github.com/MatsuriDayo/nekoray/releases)
+
+Выбираем ядро xray, добавляем профиль (конфиг-ссылка), настраиваем маршруты (копируем из `misc/customgeo4nekoray.txt`), выбираем режим
+системного прокси, и запускаем!
+
+![](figs/nekoray-1.jpg)
+
+![](figs/nekoray-2.jpg)
+
+![](figs/nekoray-3.jpg)
+
+![](figs/nekoray-4.jpg)
+
+![](figs/nekoray-5.jpg)
+
+![](figs/nekoray-6.jpg)
diff --git a/README.md b/README.md
index e81a66e..54f4bbe 100644
--- a/README.md
+++ b/README.md
@@ -1,77 +1,124 @@
 # easy-xray
 
-*Script for Linux which makes XRay installation and configuration easy*
+*Script for Linux which makes XRay management easy*
 
 (todo) [Readme in Russian](README.ru.md)
 
 (todo) [Readme in Chinese](README.cn.md)
 
-[XRay (aka ProjectX)](https://xtls.github.io/en/) is a frontier solution to surpass the internet censorship. It can work as a server and as
-a client allowing to guide traffic through a VPS outside the region of censorship. XRay configuration can be confusing for a newcomer,
-so, here is a script which helps to
+[XRay (aka ProjectX)](https://xtls.github.io/en/) is a frontier solution to circumvent the internet censorship. XRay allows to guide traffic
+through a server (VPS) outside the region of censorship as a proxie, but connection to xray server looks for authorities as a typical
+connection to a regular website. Attempts to detect VPN such as [active probing](https://ensa.fi/active-probing/) or blocking by the rule
+"all except https" are eliminated by XRay.  Also, XRay server can be configured to open only foreign websites, thus preventing detection by
+the code on domestic websites.  As a proxy, XRay has no need to encrypt already encrypted https traffic, hence CPU load is low. XRay doesn't
+need to keep the connection alive, and users don't need to manually reconnect to it time-to-time. Also users don't need to turn a client off
+to go to most domestic websites.
 
-- install/upgrade/remove XRay and geodata
+![xray-schematic: traffic to foreign websites goes through vps, traffic to domestic sites goes directly from pc](figs/xray-schematic.png)
+
+Besides of its plusses, configuration and management of XRay server is quite sophisticated. So, here is a script which helps to do it. It
+can
+
+- install/upgrade/remove XRay
 - generate credentials and server/client configs
 - add/delete users to the configs
+- and more
+
+### How to use on VPS
+
+First you need a Linux server (VPS) with [jq](https://jqlang.github.io/jq/) and `openssl` installed, they can be found in repositories of
+almost all popular Linux distributions. Then download whole `easy-xray` folder to the VPS, make the script `ex.sh` executable, and run a
+desired command with it. Use `./ex.sh help` to see the list of all available commands and `./ex.sh install` to start interactive prompt
+that installs and configures XRay.
 
-First, make the script `ex.sh` executable, then run a desired command with it. Use `./ex.sh help` to see the list of all available commands
-and `./ex.sh install` to start interactive prompt which installs and configures XRay.
 ```
 $ chmod +x ex.sh
 $ ./ex.sh help
 $ sudo ./ex.sh install
 ```
 
-### Prerequisites
+Now you have `conf` folder with server and client configs and some user configs. Server config is properly installed and XRay is running.
+Time to share configs or *links* with users! To generate config in the link form, use `./ex.sh link user_config_file.json`.
 
-For manipulations with configs, [jq](https://jqlang.github.io/jq/) is needed, it can be found in repositories of almost all popular Linux
-distributions.
+### Clients
 
-### How it works
+#### Linux
 
-With current configs, XRay creates a tunnel between the client (your laptop, phone etc.) and the server (your VPS). The tunnel uses VLESS
-[Reality](https://github.com/XTLS/REALITY/blob/main/README.en.md) protocol that obfuscates traffic and in our case imitates
-[grpc](https://en.wikipedia.org/wiki/GRPC). For the censor the tunnel looks like a usual connection to a site. The server responds to https
-requests as some popular site thus it is not suspicious for an active probing. On the client side *XRay* creates http/https and socks
-proxies which can be used by your Telegram or web browser like this:
+XRay itself can be a client, besides plenty of GUI clients that are available for other popular operating systems (see below). You can
+manually install XRay with [official script](https://github.com/XTLS/Xray-install) and manually copy `customgeo.dat` to
+`/usr/local/share/xray/` or just install them both with `sudo ./ex.sh install` command. Then, copy client config from the server and run one
+of these:
 
-![browser proxy: http/https proxy 127.0.0.1 at port 801, socks v5 host 127.0.0.1 at port 800](browser-proxy-settings.png)
-
-Note that there is no additional encryption layer in VLESS; using it you rely on the encryption that the browser (Telegram app etc.) makes.
-Note also that for current client config, traffic to .cn, .ru, .by and .ir sites goes directly from the client, see
-[here](https://github.com/EvgenyNerush/coherence-grabber) for details. This makes your server much less attention-grabbing and suspicious,
-but your connection less anonymous.
-
-### More deep description??
-
-Important: It is assumed that configs are stored and updated
-locally as `config_server.json`, `config_client.json` or
-`config_client_username.json` files. You should manually
-start XRay with one of configs, depending
-which role - server or client - XRay should play:
 ```
-    sudo cp config_(role).json /usr/local/etc/xray/config.json
+    sudo cp config_client_username.json /usr/local/etc/xray/config.json
     sudo systemctl start xray
 ```
+
 or
+
 ```
-    sudo xray run -c config_(role).json
+    sudo xray run -c config_client_username.json
 ```
 
-### Other clients
+In the current configuration, on the client side XRay creates http/https and socks5 proxies on your PC which can be used by your Telegram
+app or Web browser like this:
 
-For Windows, MacOS or Android you can try Nekobox, v2rayNG or ? (TODO): tests and config generation for them.
+![browser proxy: http/https proxy 127.0.0.1 at port 801, socks v5 host 127.0.0.1 at port 800](figs/browser-proxy-settings.png)
+
+To check that traffic to domestic and foreing sites goes by different ways, visit, for example,
+[whatismyip.com](https://www.whatismyip.com/) and [2ip.ru](https://2ip.ru/). They should show different IP addressess.
+
+#### Windows
+
+Use [Nekoray (Nekobox)](https://github.com/MatsuriDayo/nekoray) client that releases can be found on [this
+page](https://github.com/MatsuriDayo/nekoray/releases). Choose one of Assets, for instance `nekoray-3.26-2023-12-09-windows64.zip`, download
+then unzip it and run Nekoray. The following configuration is [quite easy (RU)](Nekoray.ru.md).
+
+#### MacOS
+
+Use XRay:
+
+```
+brew install xray
+cp customgeo.dat /usr/local/share/xray/ # not yet tested
+sudo xray -config=config_client_username.json
+```
+
+#### Android
+
+For many mobile applications it is enough to paste a client config in a link form from the buffer, and add customgeo in an appropriate form
+(see `misc` dir) to somethere like `Settings/Routing/Custom rules/Direct URL`. Tested applications are listed below.
+
+Use [V2RayNG](https://play.google.com/store/apps/details?id=com.v2ray.ang&pcampaignid=web_share),
+[HiddifyNG](https://play.google.com/store/apps/details?id=ang.hiddify.com&pcampaignid=web_share) or [Hiddify
+Next](https://play.google.com/store/apps/details?id=app.hiddify.com&pcampaignid=web_share). They are very similar to each other, here are
+some instructions for [V2RayNG (RU)](V2RayNG.ru.md) and [HiddifyNG (EN)](HiddifyNG.en.md).
+
+#### iOS
+
+Use [Straisand](https://apps.apple.com/us/app/streisand/id6450534064). Its configuration is very similar to that of V2Ray and Hiddify (see
+above). Manual copy-paste from json config file is also possible. (customgeo not yet tested)
+
+#### Others
+
+[Here](https://github.com/xtls/xray-core) you can find an additional list of clients.
+
+### Tor
+
+Most of GUI clients are based on xray core, but do not fully support its configuration, that is crutial for Tor. To use
+[TorBrowser](https://www.torproject.org/download/) in this case, use bridges. To get a bridge, send a letter to bridges@torproject.org, then
+copy symbols after `obfs4` and paste them to TorBrowser bridge settings.
 
 ### Futher reading
 
-Template configs contain comments and links and are good start to find another interesting Xray configuration options.
+Template configs contain comments and links and are a good start to find another interesting Xray configuration options.
 
-### Acknowledgements
+See [this link](https://github.com/EvgenyNerush/coherence-grabber) for details on how `customgeo` files are generated.
 
-[This article (in Russian)](https://habr.com/ru/articles/731608/) helped me to install *XRay* for the first time.
+[This article (in Russian)](https://habr.com/ru/articles/731608/) helped me to install XRay for the first time.
+
+The template configs are based on these [gRPC](https://github.com/XTLS/Xray-examples/tree/main/VLESS-gRPC-REALITY)
+and [XTLS](https://github.com/XTLS/Xray-examples/tree/main/VLESS-TCP-XTLS-Vision-REALITY) examples.
 
 [XRay config reference](https://xtls.github.io/en/config/) is brilliant and helped me much.
 
-Configs for [gRPC](https://github.com/XTLS/Xray-examples/tree/main/VLESS-gRPC-REALITY)
-and [XTLS](https://github.com/XTLS/Xray-examples/tree/main/VLESS-TCP-XTLS-Vision-REALITY) on that the template configs are based.
-
diff --git a/V2RayNG.ru.md b/V2RayNG.ru.md
new file mode 100644
index 0000000..5caf3c8
--- /dev/null
+++ b/V2RayNG.ru.md
@@ -0,0 +1,11 @@
+[V2RayNG в Google Play](https://play.google.com/store/apps/details?id=com.v2ray.ang&pcampaignid=web_share)
+
+Конфиг-ссылку нужно скопировать и добавить в приложение ("+" справа вверху, далее импорт профиля из буфера обмена).
+Для нормальной работы российских сайтов нужно скопировать содержимое `misc/customgeo4hiddify.txt` в пользовательские правила (прямые).
+
+![](figs/v2ray-1.jpg)
+
+![](figs/v2ray-2.jpg)
+
+![](figs/v2ray-3.jpg)
+
diff --git a/browser-proxy-settings.png b/browser-proxy-settings.png
deleted file mode 100644
index 92e631c3642f6d3c286237f36d7aefc6030a945a..0000000000000000000000000000000000000000
GIT binary patch
diff --git a/ex.sh b/ex.sh
index 83a5646..419496d 100755
--- a/ex.sh
+++ b/ex.sh
@@ -543,7 +543,7 @@ then
     public_key=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.publicKey" $conf_file))
     server_name=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.serverName" $conf_file))
     short_id=$(strip_quotes $(jq ".outbounds[0].streamSettings.realitySettings.shortId" $conf_file))
-    link="vless://${id}@${address}:${port}?fragment=&security=reality&encryption=none&pbk=${public_key}&headerType=none&fp=chrome&type=tcp&flow=xtls-rprx-vision&sni=${server_name}&sid=${short_id}#easy-xray+%F0%9F%97%BD"
+    link="vless://${id}@${address}:${port}?fragment=&security=reality&encryption=none&pbk=${public_key}&fp=chrome&type=tcp&flow=xtls-rprx-vision&sni=${server_name}&sid=${short_id}#easy-xray+%F0%9F%97%BD"
     echo -e "${yellow}don't forget to share misc/customgeo4hiddify.txt as well
 ${green}here is your link:${normal}"
     echo $link
@@ -649,7 +649,7 @@ then
                 [[ $SUDO_USER ]] && chown "$SUDO_USER:$SUDO_USER" ${to}/config_server.json
                 if $ok1 && $ok2
                 then
-                    echo -e "${green}${to}/config_client_${uname_from_email}.json is written, config_server.json is updated${normal}"
+                    echo -e "${green}${to}/config_client_${uname_from_email}.json is written, ${to}/config_server.json is updated${normal}"
                 else
                     echo -e "${yellow}something went wrong with username ${uname_from_email}${normal}"
                 fi
diff --git a/figs/browser-proxy-settings.png b/figs/browser-proxy-settings.png
new file mode 100644
index 0000000000000000000000000000000000000000..6901bd418658fc72a7f755e1bd356dd6d838b48c
GIT binary patch
diff --git a/figs/hiddify-1.jpg b/figs/hiddify-1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..151d95e61a5d0b32b72a4cf90782c0649bd0e531
GIT binary patch
diff --git a/figs/hiddify-2.jpg b/figs/hiddify-2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..9cbbe38aea6887ed9f3f34e709e79072cfd38f6e
GIT binary patch
diff --git a/figs/hiddify-3.jpg b/figs/hiddify-3.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..61d3476e7dc46b143e47b48375aea6855ac54ddc
GIT binary patch
diff --git a/figs/hiddify-4.jpg b/figs/hiddify-4.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..89484a7fd3b41c0fb1b44ca249b62ee6ef50bbdb
GIT binary patch
diff --git a/figs/nekoray-1.jpg b/figs/nekoray-1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..395a2ab55b256e27e779e1be85c051800faf8159
GIT binary patch
diff --git a/figs/nekoray-2.jpg b/figs/nekoray-2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3619f2a4f3876998d6a0b6c51c9c347da8548085
GIT binary patch
diff --git a/figs/nekoray-3.jpg b/figs/nekoray-3.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..ae4a5cf87bf5ba7ab27c6144eefa34fbb75395e6
GIT binary patch
diff --git a/figs/nekoray-5.jpg b/figs/nekoray-5.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..36a07e1e41631387854c46b82d2f31db9b9a9cf2
GIT binary patch
diff --git a/figs/nekoray-6.jpg b/figs/nekoray-6.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..131309b2bda8a8fd549d718ae3d7550a114df009
GIT binary patch
diff --git a/figs/v2ray-1.jpg b/figs/v2ray-1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..a1161fd20a134bb7262ed3723c27b067f45333fe
GIT binary patch
diff --git a/figs/v2ray-2.jpg b/figs/v2ray-2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..580b36e35820effbc3e70f2e3f82e930e52cc577
GIT binary patch
diff --git a/figs/v2ray-3.jpg b/figs/v2ray-3.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f043357c6036df84f5c5c5374105a3cf2804d9ad
GIT binary patch
diff --git a/figs/xray-schematic.png b/figs/xray-schematic.png
new file mode 100644
index 0000000000000000000000000000000000000000..e14bc4dc5437e14cf64b0bc9f8de59b29c6fd36f
GIT binary patch

From d4c0d6f009186f8b9367d1d8d68ed0ee8b7d7bbc Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Fri, 15 Dec 2023 16:23:43 +0300
Subject: [PATCH 15/17] fix spaces, add note about bittorrent

---
 README.md | 19 +++++++++++--------
 1 file changed, 11 insertions(+), 8 deletions(-)

diff --git a/README.md b/README.md
index 54f4bbe..9949b30 100644
--- a/README.md
+++ b/README.md
@@ -32,9 +32,9 @@ desired command with it. Use `./ex.sh help` to see the list of all available com
 that installs and configures XRay.
 
 ```
-$ chmod +x ex.sh
-$ ./ex.sh help
-$ sudo ./ex.sh install
+chmod +x ex.sh
+./ex.sh help
+sudo ./ex.sh install
 ```
 
 Now you have `conf` folder with server and client configs and some user configs. Server config is properly installed and XRay is running.
@@ -46,18 +46,17 @@ Time to share configs or *links* with users! To generate config in the link form
 
 XRay itself can be a client, besides plenty of GUI clients that are available for other popular operating systems (see below). You can
 manually install XRay with [official script](https://github.com/XTLS/Xray-install) and manually copy `customgeo.dat` to
-`/usr/local/share/xray/` or just install them both with `sudo ./ex.sh install` command. Then, copy client config from the server and run one
-of these:
+`/usr/local/share/xray/` or just install them both with `sudo ./ex.sh install` command. Then, copy client config from the server and run:
 
 ```
-    sudo cp config_client_username.json /usr/local/etc/xray/config.json
-    sudo systemctl start xray
+sudo cp config_client_username.json /usr/local/etc/xray/config.json
+sudo systemctl start xray
 ```
 
 or
 
 ```
-    sudo xray run -c config_client_username.json
+sudo xray run -c config_client_username.json
 ```
 
 In the current configuration, on the client side XRay creates http/https and socks5 proxies on your PC which can be used by your Telegram
@@ -109,6 +108,10 @@ Most of GUI clients are based on xray core, but do not fully support its configu
 [TorBrowser](https://www.torproject.org/download/) in this case, use bridges. To get a bridge, send a letter to bridges@torproject.org, then
 copy symbols after `obfs4` and paste them to TorBrowser bridge settings.
 
+### Bittorrent
+
+Bittorent protocol is blocked in the current configuration. Using bittorent on a VPS can lead to a ban from VPS provider.
+
 ### Futher reading
 
 Template configs contain comments and links and are a good start to find another interesting Xray configuration options.

From 00df2d8f109ff9b6d91dcf11eb878d9c4913527b Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Sat, 16 Dec 2023 23:25:36 +0300
Subject: [PATCH 16/17] add remark about RealiTLScanner

---
 README.md | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 9949b30..70566c9 100644
--- a/README.md
+++ b/README.md
@@ -112,7 +112,22 @@ copy symbols after `obfs4` and paste them to TorBrowser bridge settings.
 
 Bittorent protocol is blocked in the current configuration. Using bittorent on a VPS can lead to a ban from VPS provider.
 
-### Futher reading
+### What else
+
+#### Hints
+
+To choose a good `serverName` (your VPS will mimic this website), you can use [RealiTLScanner](https://github.com/XTLS/RealiTLScanner). This
+tool scans ip addresses near your server (do it not from your VPS!) and show names found at port 443. With `nmap -T4 hostname` you can check
+that only ports 80 and 443 are open on hosts of the found websites (as in the configurations of template configs). It is also good to move
+ssh on your VPS to a port beyond 1024. It can be done in `/etc/ssh/sshd_config`. Check twice that VPS is available at your new port before
+you comment Port 22!
+
+```
+#Port 22
+Port 43210
+```
+
+#### Links
 
 Template configs contain comments and links and are a good start to find another interesting Xray configuration options.
 

From 9de43ef78a3708939fd40e14ccb2fab5bcdb7b7c Mon Sep 17 00:00:00 2001
From: Evgeny Nerush <eugene.nerush@gmail.com>
Date: Sat, 23 Dec 2023 22:47:12 +0300
Subject: [PATCH 17/17] add RU readme

---
 README.md    |   4 +-
 README.ru.md | 148 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 149 insertions(+), 3 deletions(-)
 create mode 100644 README.ru.md

diff --git a/README.md b/README.md
index 70566c9..a83bbea 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,7 @@
 
 *Script for Linux which makes XRay management easy*
 
-(todo) [Readme in Russian](README.ru.md)
-
-(todo) [Readme in Chinese](README.cn.md)
+[Readme in Russian](README.ru.md) [(todo) Readme in Chinese](README.cn.md)
 
 [XRay (aka ProjectX)](https://xtls.github.io/en/) is a frontier solution to circumvent the internet censorship. XRay allows to guide traffic
 through a server (VPS) outside the region of censorship as a proxie, but connection to xray server looks for authorities as a typical
diff --git a/README.ru.md b/README.ru.md
new file mode 100644
index 0000000..1113700
--- /dev/null
+++ b/README.ru.md
@@ -0,0 +1,148 @@
+# easy-xray
+
+*XRay - это просто*
+
+[(EN)](README.md) [(CN(todo))](README.cn.md)
+
+[XRay (aka ProjectX)](https://xtls.github.io/en/) - современное решение для обхода интернет-цензуры. Он - как прокси - позволяет проводить
+трафик через сервер (VPS) за пределами региона с цензурой, при этом подключение к серверу XRay выглядит как подключение к обычному сайту.
+Попытки обнаружения VPN, такие как активное зондирование [(active probing)](https://ensa.fi/active-probing/) или блокировка по правилу
+«блокируем все протоколы, кроме https», для XRay не страшны. Кроме того, сервер XRay можно настроить на открытие только "зарубежных"
+веб-сайтов, что предотвращает попытки обнаружения туннеля сайтами "домашнего" региона.  XRay (в случае использования протокола
+VLESS-Reality) не шифрует уже зашифрованный https-трафик, поэтому нагрузка на процессор невелика. XRay не нужно поддерживать соединение, и
+пользователям не нужно время от времени повторно подключаться к нему вручную. При правильной настройке клиента пользователям не нужно
+отключаться от сервера, чтобы перейти на большинство веб-сайтов "домашнего" региона.
+
+![схема: трафик до зарубежных сайтов идёт через сервер, а до домашних сайтов - напрямую с ПК](figs/xray-schematic.png)
+
+Помимо плюсов, у XRay есть и недостатки - например, сложная настройка и администрирование, что стало мотивацией для создания этого
+репозитория и скрипта `ex.sh`. Этот скрипт поможет
+
+- установить/обновить/удалить XRay
+- генерировать учетные данные и конфигурации сервера/клиента
+- добавлять/удалять пользователей
+- и т. д.
+
+### Как использовать на VPS
+
+Для начала нужно арендовать VPS сервер вне региона с цензурой (или попросить друзей заграницей сделать это). После на сервер нужно
+установить [jq](https://jqlang.github.io/jq/) и `openssl` - их можно найти в репозиторияx практически всех популярных Linux дистрибутивов.
+Затем скачайте `easy-xray` (всю директорию целиком), сделайте исполняемым файл `ex.sh` и запустите нужную команду. Используйте `./ex.sh
+help` для получения списка всех команд и короткой справки по ним. Для установки и настройки xray используйте `./ex.sh install` - эта команда
+предложит вам ввести ip-адрес вашего сервера, адрес сайта, под который ваш сервер будет маскироваться, и имена пользователей.
+
+```
+chmod +x ex.sh
+./ex.sh help
+sudo ./ex.sh install
+```
+
+В результате у вас появится директория `conf` с серверным конфигом, основным клиентским конфигом и конфигами пользователей. Серверный конфиг
+будет должным образом установлен, а xray - запущен с ним. Настало время раздать конфиги пользователям! Для многих графических клиентов
+удобно использовать конфиги в виде ссылок, которые можно сгенерировать командой `./ex.sh link user_config_file.json`.
+
+### Клиенты
+
+#### Linux
+
+Для подключения к XRay-серверу существуют клиенты с графическим интерфейсом, но о них чуть позже. На Linux удобно использовать сам XRay в
+качестве клиента.  XRay можно установить вручную, используя [официальный скрипт](https://github.com/XTLS/Xray-install), после чего
+скопировать `customgeo.dat` в `/usr/local/share/xray/`. Другой вариант установить XRay и `customgeo.dat` - воспользоваться командой `sudo
+./ex.sh install`. Затем нужно скопировать клиентский конфиг с сервера и выполнить
+
+```
+sudo cp config_client_username.json /usr/local/etc/xray/config.json
+sudo systemctl start xray
+```
+
+или
+
+```
+sudo xray run -c config_client_username.json
+```
+
+XRay с текущим клиентским конфигом создаёт локальные http/https и socks5 прокси, которые могут быть прописаны в настройки Телеграма или
+браузера примерно так:
+
+![прокси в браузере: http/https прокси 127.0.0.1 порт 801, socks5 хост 127.0.0.1 порт 800](figs/browser-proxy-settings.png)
+
+Чтобы проверить, что трафик до "зарубежных" и "домашних" сайтов идёт разными путями, зайдите на
+[whatismyip.com](https://www.whatismyip.com/) и [2ip.ru](https://2ip.ru/). Они должны показать разные ip-адреса и локации.
+
+#### Windows
+
+Используйте [Nekoray (Nekobox)](https://github.com/MatsuriDayo/nekoray) - клиент с графическим интерфейсом. Релизы для Windows можно найти
+на [этой странице](https://github.com/MatsuriDayo/nekoray/releases) в разделе Assets. Выберите и скачайте, например,
+`nekoray-3.26-2023-12-09-windows64.zip`, затем распакуйте и запустите. Дальнейшее конфигурирование [описано здесь](Nekoray.ru.md).
+
+#### MacOS
+
+Используйте сам XRay:
+
+```
+brew install xray
+cp customgeo.dat /usr/local/share/xray/ # not yet tested
+sudo xray -config=config_client_username.json
+```
+
+#### Android
+
+Для многих мобильных клиентов вся настройка заключается в том, что нужно вставить конфиг-ссылку из буфера обмена, а после добавить сайты из
+customgeo в подходящей форме (см. директорию `misc`) в "прямые" маршруты (в `Settings/Routing/Custom rules/Direct URL` или что-то подобное).
+Приложения, которые были протестированы, перечислены ниже.
+
+Скачайте из Google Play [V2RayNG](https://play.google.com/store/apps/details?id=com.v2ray.ang&pcampaignid=web_share),
+[HiddifyNG](https://play.google.com/store/apps/details?id=ang.hiddify.com&pcampaignid=web_share) или [Hiddify
+Next](https://play.google.com/store/apps/details?id=app.hiddify.com&pcampaignid=web_share). Они очень похожи друг на друга, здесь есть
+инструкции для [V2RayNG](V2RayNG.ru.md) и [HiddifyNG (EN)](HiddifyNG.en.md).
+
+#### iOS
+
+Используйте [Straisand](https://apps.apple.com/us/app/streisand/id6450534064). Конфигурирование аналогично тому, что описано для V2Ray или
+Hiddify (см. выше). Customgeo - ещё не протестирован.
+
+#### Другие
+
+[Здесь](https://github.com/xtls/xray-core) можно найти дополнительный список клиентов.
+
+### Tor
+
+В случае использования XRay в качестве клиента с текущей конфигурацией не должно возникнуть проблем. Однако большинство графических
+клиентов, хотя в качестве ядра используют xray, не дают полного доступа к его конфигурации. Из-за этого
+[Torbrowser](https://www.torproject.org/download/) не всегда может работать "из коробки". Если подключиться к сети Tor не удаётся,
+попробуйте использовать мост (специальный промежуточный сервер между вами и сетью Tor). Чтобы получить мост, отправьте письмо с любым
+текстом на bridges@torproject.org, затем вставьте из ответного письма символы после `obfs4` в настройки моста в Torbrowser.
+
+### Bittorrent
+
+Bittorent протокол блокируется в текущих настройках - использование bittorrent может привести к бану со стороны VPS провайдера.
+
+### Что ещё?
+
+#### Советы
+
+Для того, чтобы выбрать сайт, который будет видеть на вашем сервере цензор в случае атаки active probing (`serverName` в конфигах), можно
+воспользоваться утилитой [RealiTLScanner](https://github.com/XTLS/RealiTLScanner). Она может просканировать адреса, близкие к вашему серверу
+(делайте это с домашнего компьютера, а не с сервера!), и показать имена сайтов на них. С помощью команды `nmap -T4 hostname` можно
+проверить, что на выбранном вами сервере открыты только порты 80 и 443 (как в настройках xray здесь). Также неплохо выбрать далёкий порт для
+ssh вместо 22-го, если на выбранном вами сайте этот порт не открыт. Дважды проверьте, что по выбранному вами порту ssh вы можете зайти на
+сервер - только после этого закрывайте порт 22.
+
+```
+#Port 22
+Port 43210
+```
+
+#### Ссылки
+
+Шаблонные конфиг-файлы для сервера и клиента (`template_config_*.json`) содержат много комментариев, которые могут помочь в создании своей
+собственной конфигурации клиента и сервера.
+
+Шаблонные конфиги сделаны на основе этих [двух](https://github.com/XTLS/Xray-examples/tree/main/VLESS-gRPC-REALITY) [примеров](https://github.com/XTLS/Xray-examples/tree/main/VLESS-TCP-XTLS-Vision-REALITY).
+
+[Описание всех возможных полей XRay конфигов (EN)](https://xtls.github.io/en/config/) очень подробное и хорошо помогает.
+
+[Здесь](https://github.com/EvgenyNerush/coherence-grabber) детали того, как собраны сайты для доступа напрямую, прописанные в `customgeo`.
+
+[Эта статья](https://habr.com/ru/articles/731608/) помогла мне установить XRay в первый раз.
+
