Browse Source

[feature] multi-user shadowsocks @alireza0

Co-Authored-By: Alireza Ahmadi <[email protected]>
MHSanaei 1 year ago
parent
commit
f22dd6b53d

+ 88 - 74
web/assets/js/model/xray.js

@@ -1012,66 +1012,6 @@ class Inbound extends XrayCommonClass {
         return this.network === "http";
     }
 
-    // VMess & VLess
-    get uuid() {
-        switch (this.protocol) {
-            case Protocols.VMESS:
-                return this.settings.vmesses[0].id;
-            case Protocols.VLESS:
-                return this.settings.vlesses[0].id;
-            default:
-                return "";
-        }
-    }
-
-    // VLess & Trojan
-    get flow() {
-        switch (this.protocol) {
-            case Protocols.VLESS:
-                return this.settings.vlesses[0].flow;
-            case Protocols.TROJAN:
-                return this.settings.trojans[0].flow;
-            default:
-                return "";
-        }
-    }
-
-    // VMess
-    get alterId() {
-        switch (this.protocol) {
-            case Protocols.VMESS:
-                return this.settings.vmesses[0].alterId;
-            default:
-                return "";
-        }
-    }
-
-    // Socks & HTTP
-    get username() {
-        switch (this.protocol) {
-            case Protocols.SOCKS:
-            case Protocols.HTTP:
-                return this.settings.accounts[0].user;
-            default:
-                return "";
-        }
-    }
-
-    // Trojan & Shadowsocks & Socks & HTTP
-    get password() {
-        switch (this.protocol) {
-            case Protocols.TROJAN:
-                return this.settings.trojans[0].password;
-            case Protocols.SHADOWSOCKS:
-                return this.settings.password;
-            case Protocols.SOCKS:
-            case Protocols.HTTP:
-                return this.settings.accounts[0].pass;
-            default:
-                return "";
-        }
-    }
-
     // Shadowsocks
     get method() {
         switch (this.protocol) {
@@ -1146,9 +1086,13 @@ class Inbound extends XrayCommonClass {
                     return this.settings.vlesses[index].expiryTime < new Date().getTime();
                 return false
                 case Protocols.TROJAN:
-                    if(this.settings.trojans[index].expiryTime > 0)
-                        return this.settings.trojans[index].expiryTime < new Date().getTime();
-                    return false
+                if(this.settings.trojans[index].expiryTime > 0)
+                    return this.settings.trojans[index].expiryTime < new Date().getTime();
+                return false
+            case Protocols.SHADOWSOCKS:
+                if(this.settings.shadowsockses[index].expiryTime > 0)
+                    return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
+                return false
             default:
                 return false;
         }
@@ -1159,7 +1103,6 @@ class Inbound extends XrayCommonClass {
             case Protocols.VMESS:
             case Protocols.VLESS:
             case Protocols.TROJAN:
-            case Protocols.SHADOWSOCKS:
                 break;
             default:
                 return false;
@@ -1228,7 +1171,6 @@ class Inbound extends XrayCommonClass {
             case Protocols.VMESS:
             case Protocols.VLESS:
             case Protocols.TROJAN:
-            case Protocols.SHADOWSOCKS:
                 return true;
             default:
                 return false;
@@ -1443,13 +1385,11 @@ class Inbound extends XrayCommonClass {
         return url.toString();
     }
 
-    genSSLink(address='', remark='') {
+    genSSLink(address='', remark='', clientIndex = 0) {
         let settings = this.settings;
-        const server = this.stream.tls.server;
-        if (!ObjectUtil.isEmpty(server)) {
-            address = server;
-        }
-        return 'ss://' + safeBase64(settings.method + ':' + settings.password) + `@${address}:${this.port}#${encodeURIComponent(remark)}`;
+        const port = this.port;
+
+        return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark);
     }
 
     genTrojanLink(address = '', remark = '', clientIndex = 0) {
@@ -1569,7 +1509,11 @@ class Inbound extends XrayCommonClass {
                     remark += '-' + this.settings.vlesses[clientIndex].email
                 }
                 return this.genVLESSLink(address, remark, clientIndex);
-            case Protocols.SHADOWSOCKS: return this.genSSLink(address, remark);
+            case Protocols.SHADOWSOCKS: 
+                if (this.settings.shadowsockses[clientIndex].email != ""){
+                    remark = this.settings.shadowsockses[clientIndex].email
+                }
+                return this.genSSLink(address, remark, clientIndex);
             case Protocols.TROJAN:
                 if (this.settings.trojans[clientIndex].email != ""){
                     remark += '-' + this.settings.trojans[clientIndex].email
@@ -2033,13 +1977,15 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
 Inbound.ShadowsocksSettings = class extends Inbound.Settings {
     constructor(protocol,
                 method=SSMethods.BLAKE3_AES_256_GCM,
-                password=RandomUtil.randomSeq(44),
-                network='tcp,udp'
+                password=RandomUtil.randomShadowsocksPassword(),
+                network='tcp,udp',
+                shadowsockses=[new Inbound.ShadowsocksSettings.Shadowsocks()]
     ) {
         super(protocol);
         this.method = method;
         this.password = password;
         this.network = network;
+        this.shadowsockses = shadowsockses;
     }
 
     static fromJson(json={}) {
@@ -2048,6 +1994,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
             json.method,
             json.password,
             json.network,
+            json.clients.map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),
         );
     }
 
@@ -2056,10 +2003,77 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
             method: this.method,
             password: this.password,
             network: this.network,
+            clients: Inbound.ShadowsocksSettings.toJsonArray(this.shadowsockses)
         };
     }
 };
 
+Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
+    constructor(password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
+        super();
+        this.password = password;
+        this.email = email;
+        this.limitIp = limitIp;
+        this.totalGB = totalGB;
+        this.expiryTime = expiryTime;
+        this.enable = enable;
+        this.tgId = tgId;
+        this.subId = subId;
+    }
+
+    toJson() {
+        return {
+            password: this.password,
+            email: this.email,
+            limitIp: this.limitIp,
+            totalGB: this.totalGB,
+            expiryTime: this.expiryTime,
+            enable: this.enable,
+            tgId: this.tgId,
+            subId: this.subId,
+        };
+    }
+
+    static fromJson(json = {}) {
+        return new Inbound.ShadowsocksSettings.Shadowsocks(
+            json.password,
+            json.email,
+            json.limitIp,
+            json.totalGB,
+            json.expiryTime,
+            json.enable,
+            json.tgId,
+            json.subId,
+        );
+    }
+
+    get _expiryTime() {
+        if (this.expiryTime === 0 || this.expiryTime === "") {
+            return null;
+        }
+        if (this.expiryTime < 0){
+            return this.expiryTime / -86400000;
+        }
+        return moment(this.expiryTime);
+    }
+
+    set _expiryTime(t) {
+        if (t == null || t === "") {
+            this.expiryTime = 0;
+        } else {
+            this.expiryTime = t.valueOf();
+        }
+    }
+    get _totalGB() {
+        return toFixed(this.totalGB / ONE_GB, 2);
+    }
+
+    set _totalGB(gb) {
+        this.totalGB = toFixed(gb * ONE_GB, 0);
+    }
+
+};
+
 Inbound.DokodemoSettings = class extends Inbound.Settings {
     constructor(protocol, address, port, network='tcp,udp', followRedirect=false) {
         super(protocol);

+ 6 - 0
web/assets/js/util/utils.js

@@ -165,6 +165,12 @@ class RandomUtil {
         str += this.randomShortIdSeq(8)
         return str;
     }
+    
+    static randomShadowsocksPassword(){
+        let array = new Uint8Array(32);
+        window.crypto.getRandomValues(array);
+        return btoa(String.fromCharCode.apply(null, array));
+    }
 }
 
 class ObjectUtil {

+ 10 - 1
web/html/xui/client_modal.html

@@ -44,7 +44,7 @@
                 if (this.clients[index].expiryTime < 0){
                     this.delayedStart = true;
                 }
-                this.oldClientId = this.dbInbound.protocol == "trojan" ? this.clients[index].password : this.clients[index].id;
+                this.oldClientId = this.getClientId(dbInbound.protocol,clients[index]);
             } else {
                 this.addClient(this.inbound.protocol, this.clients);
             }
@@ -56,14 +56,23 @@
                 case Protocols.VMESS: return clientSettings.vmesses;
                 case Protocols.VLESS: return clientSettings.vlesses;
                 case Protocols.TROJAN: return clientSettings.trojans;
+                case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
                 default: return null;
             }
         },
+        getClientId(protocol, client) {
+            switch(protocol){
+                case Protocols.TROJAN: return client.password;
+                case Protocols.SHADOWSOCKS: return client.email;
+                default: return client.id;
+            }
+        },
         addClient(protocol, clients) {
             switch (protocol) {
                 case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
                 case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
                 case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
+                case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks());
                 default: return null;
             }
         },

+ 2 - 1
web/html/xui/form/client.html

@@ -18,7 +18,8 @@
     <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
         <a-switch v-model="client.enable"></a-switch>
     </a-form-item>
-    <a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN">
+    <a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
+        <a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
         <a-input v-model.trim="client.password" style="width: 150px;" ></a-input>
     </a-form-item>
     <a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">

+ 82 - 1
web/html/xui/form/protocol/shadowsocks.html

@@ -1,5 +1,87 @@
 {{define "form/shadowsocks"}}
 <a-form layout="inline">
+    <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
+        <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
+            <a-form-item>
+                <span slot="label">
+                    <span>{{ i18n "pages.inbounds.Email" }}</span>
+                    <a-tooltip>
+                        <template slot="title">
+                            <span>{{ i18n "pages.inbounds.EmailDesc" }}</span>
+                        </template>
+                        <a-icon @click="getNewEmail(client)" type="sync"></a-icon>
+                    </a-tooltip>
+                </span>
+                <a-input v-model.trim="client.email" style="width: 150px;"></a-input>
+            </a-form-item>
+            <a-form-item label="Password">
+                <a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
+                <a-input v-model.trim="client.password" style="width: 150px;"></a-input>
+            </a-form-item>
+            <a-form-item label="Subscription" v-if="client.email">
+                <a-input v-model.trim="client.subId"></a-input>
+            </a-form-item>
+            <a-form-item label="Telegram Username" v-if="client.email">
+                <a-input v-model.trim="client.tgId"></a-input>
+            </a-form-item>
+            <a-form-item>
+                <span slot="label">
+                    <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
+                    <a-tooltip>
+                        <template slot="title">
+                            <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
+                        </template>
+                        <a-icon type="question-circle" theme="filled"></a-icon>
+                    </a-tooltip>
+                </span>
+                <a-input-number v-model="client.limitIp" min="0"  style="width: 70px;"></a-input-number>
+            </a-form-item>
+            <a-form-item>
+                <span slot="label">
+                    <span >{{ i18n "pages.inbounds.totalFlow" }}</span> (GB)
+                    <a-tooltip>
+                        <template slot="title">
+                            0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
+                        </template>
+                        <a-icon type="question-circle" theme="filled"></a-icon>
+                    </a-tooltip>
+                </span>
+                <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
+            </a-form-item>
+            <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
+                <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
+            </a-form-item>
+            <a-form-item label='{{ i18n "pages.client.expireDays" }}'>
+                <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
+            </a-form-item>
+            <a-form-item>
+                <span slot="label">
+                    <span >{{ i18n "pages.inbounds.expireDate" }}</span>
+                    <a-tooltip>
+                        <template slot="title">
+                            <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
+                        </template>
+                        <a-icon type="question-circle" theme="filled"></a-icon>
+                    </a-tooltip>
+                </span>
+                <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
+                                :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
+                                v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
+            </a-form-item>
+        </a-collapse-panel>
+    </a-collapse>
+    <a-collapse v-else>
+        <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.shadowsockses.length">
+            <table width="100%">
+                <tr class="client-table-header">
+                    <th v-for="col in Object.keys(inbound.settings.shadowsockses[0]).slice(0, 3)">[[ col ]]</th>
+                </tr>
+                <tr v-for="(client, index) in inbound.settings.shadowsockses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
+                    <td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
+                </tr>
+            </table>
+        </a-collapse-panel>
+    </a-collapse>
     <a-form-item label='{{ i18n "encryption" }}'>
         <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
             <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
@@ -15,5 +97,4 @@
             <a-select-option value="udp">UDP</a-select-option>
         </a-select>
     </a-form-item>
-</a-form>
 {{end}}

+ 6 - 0
web/html/xui/form/protocol/trojan.html

@@ -53,6 +53,12 @@
             </span>
             <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
         </a-form-item>
+        <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
+            <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
+        </a-form-item>
+        <a-form-item label='{{ i18n "pages.client.expireDays" }}'>
+            <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
+        </a-form-item>
         <a-form-item>
             <span slot="label">
                 <span >{{ i18n "pages.inbounds.expireDate" }}</span>

+ 6 - 0
web/html/xui/form/protocol/vless.html

@@ -59,6 +59,12 @@
             </span>
             <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
         </a-form-item>
+        <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
+            <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
+        </a-form-item>
+        <a-form-item label='{{ i18n "pages.client.expireDays" }}'>
+            <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
+        </a-form-item>
         <a-form-item>
             <span slot="label">
                 <span >{{ i18n "pages.inbounds.expireDate" }}</span>

+ 6 - 0
web/html/xui/form/protocol/vmess.html

@@ -50,6 +50,12 @@
             </span>
             <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
         </a-form-item>
+        <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
+            <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
+        </a-form-item>
+        <a-form-item label='{{ i18n "pages.client.expireDays" }}'>
+            <a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
+        </a-form-item>
         <a-form-item>
             <span slot="label">
                 <span >{{ i18n "pages.inbounds.expireDate" }}</span>

+ 1 - 0
web/html/xui/inbound_modal.html

@@ -48,6 +48,7 @@
                 case Protocols.VMESS: return clientSettings.vmesses;
                 case Protocols.VLESS: return clientSettings.vlesses;
                 case Protocols.TROJAN: return clientSettings.trojans;
+                case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
                 default: return null;
             }
         },

+ 18 - 12
web/html/xui/inbounds.html

@@ -116,15 +116,11 @@
                                 <a-dropdown :trigger="['click']">
                                     <a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
                                     <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme">
-                                        <a-menu-item v-if="dbInbound.isSS" key="qrcode">
-                                            <a-icon type="qrcode"></a-icon>
-                                            {{ i18n "qrCode" }}
-                                        </a-menu-item>
                                         <a-menu-item key="edit">
                                             <a-icon type="edit"></a-icon>
                                             {{ i18n "edit" }}
                                         </a-menu-item>
-                                        <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
+                                        <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
                                             <a-menu-item key="addClient">
                                                 <a-icon type="user-add"></a-icon>
                                                 {{ i18n "pages.client.add"}}
@@ -168,7 +164,7 @@
                             </template>
                             <template slot="protocol" slot-scope="text, dbInbound">
                                 <a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
-                                <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
+                                <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan">
                                     <a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
                                     <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
                                     <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag>
@@ -231,7 +227,7 @@
                                     {{template "client_table"}}
                                 </a-table>
                                 <a-table
-                                v-else-if="record.protocol === Protocols.TROJAN"
+                                v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
                                 :row-key="client => client.id"
                                 :columns="innerTrojanColumns"
                                 :data-source="getInboundClients(record)"
@@ -671,7 +667,7 @@
             },
             delClient(dbInboundId,client) {
                 dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
-                clientId = dbInbound.protocol == "trojan" ? client.password : client.id;
+                clientId = this.getClientId(dbInbound.protocol,client);
                 this.$confirm({
                     title: '{{ i18n "pages.inbounds.deleteInbound"}}',
                     content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
@@ -686,9 +682,17 @@
                     case Protocols.VMESS: return clientSettings.vmesses;
                     case Protocols.VLESS: return clientSettings.vlesses;
                     case Protocols.TROJAN: return clientSettings.trojans;
+                    case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
                     default: return null;
                 }
             },
+            getClientId(protocol, client) {
+                switch(protocol){
+                    case Protocols.TROJAN: return client.password;
+                    case Protocols.SHADOWSOCKS: return client.email;
+                    default: return client.id;
+                }
+            },
             showQrcode(dbInbound, clientIndex) {
                 const link = dbInbound.genLink(clientIndex);
                 qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
@@ -707,7 +711,7 @@
                 clients = this.getClients(dbInbound.protocol, inbound.settings);
                 index = this.findIndexOfClient(clients, client);
                 clients[index].enable = !clients[index].enable;
-                clientId = dbInbound.protocol == "trojan" ? clients[index].password : clients[index].id;
+                clientId = this.getClientId(dbInbound.protocol,clients[index]);
                 await this.updateClient(clients[index],dbInboundId, clientId);
                 this.loading(false);
             },
@@ -719,11 +723,13 @@
             },
             getInboundClients(dbInbound) {
                 if(dbInbound.protocol == Protocols.VLESS) {
-                    return dbInbound.toInbound().settings.vlesses
+                    return dbInbound.toInbound().settings.vlesses;
                 } else if(dbInbound.protocol == Protocols.VMESS) {
-                    return dbInbound.toInbound().settings.vmesses
+                    return dbInbound.toInbound().settings.vmesses;
                 } else if(dbInbound.protocol == Protocols.TROJAN) {
-                    return dbInbound.toInbound().settings.trojans
+                    return dbInbound.toInbound().settings.trojans;
+                } else if(dbInbound.protocol == Protocols.SHADOWSOCKS) {
+                    return dbInbound.toInbound().settings.shadowsockses;
                 }
             },
             resetClientTraffic(client,dbInboundId) {

+ 5 - 0
web/service/inbound.go

@@ -332,6 +332,9 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) error
 	if oldInbound.Protocol == "trojan" {
 		client_key = "password"
 	}
+	if oldInbound.Protocol == "shadowsocks" {
+		client_key = "email"
+	}
 
 	inerfaceClients := settings["clients"].([]interface{})
 	var newClients []interface{}
@@ -398,6 +401,8 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
 		oldClientId := ""
 		if oldInbound.Protocol == "trojan" {
 			oldClientId = oldClient.Password
+		} else if oldInbound.Protocol == "shadowsocks" {
+			oldClientId = oldClient.Email
 		} else {
 			oldClientId = oldClient.ID
 		}

+ 24 - 0
web/service/sub.go

@@ -97,6 +97,8 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
 		return s.genVlessLink(inbound, email)
 	case "trojan":
 		return s.genTrojanLink(inbound, email)
+	case "shadowsocks":
+		return s.genShadowsocksLink(inbound, email)
 	}
 	return ""
 }
@@ -565,6 +567,28 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
 	return url.String()
 }
 
+func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
+	address := s.address
+	if inbound.Protocol != model.Shadowsocks {
+		return ""
+	}
+	clients, _ := s.inboundService.getClients(inbound)
+
+	var settings map[string]interface{}
+	json.Unmarshal([]byte(inbound.Settings), &settings)
+	inboundPassword := settings["password"].(string)
+	method := settings["method"].(string)
+	clientIndex := -1
+	for i, client := range clients {
+		if client.Email == email {
+			clientIndex = i
+			break
+		}
+	}
+	encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
+	return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, clients[clientIndex].Email)
+}
+
 func searchKey(data interface{}, key string) (interface{}, bool) {
 	switch val := data.(type) {
 	case map[string]interface{}: