mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-04-19 13:32:24 +00:00
full multiuser shadowsocks
full multiuser shadowsocks + fix logs after api changes Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
This commit is contained in:
parent
4cfed17650
commit
145ea1e6f1
8 changed files with 87 additions and 27 deletions
|
@ -755,7 +755,10 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, ex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
|
encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
|
||||||
|
if method[0] == '2' {
|
||||||
|
encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
|
||||||
|
}
|
||||||
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
|
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
|
||||||
url, _ := url.Parse(link)
|
url, _ := url.Parse(link)
|
||||||
q := url.Query()
|
q := url.Query()
|
||||||
|
|
|
@ -16,9 +16,10 @@ const VmessMethods = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const SSMethods = {
|
const SSMethods = {
|
||||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
|
||||||
AES_256_GCM: 'aes-256-gcm',
|
AES_256_GCM: 'aes-256-gcm',
|
||||||
AES_128_GCM: 'aes-128-gcm',
|
AES_128_GCM: 'aes-128-gcm',
|
||||||
|
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||||
|
XCHACHA20_POLY1305: 'xchacha20-poly1305',
|
||||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||||
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
|
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
|
||||||
|
@ -1040,7 +1041,10 @@ class Inbound extends XrayCommonClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
get isSSMultiUser() {
|
get isSSMultiUser() {
|
||||||
return [SSMethods.BLAKE3_AES_128_GCM,SSMethods.BLAKE3_AES_256_GCM].includes(this.method);
|
return this.method != SSMethods.BLAKE3_CHACHA20_POLY1305;
|
||||||
|
}
|
||||||
|
get isSS2022(){
|
||||||
|
return this.method.substring(0,4) === "2022";
|
||||||
}
|
}
|
||||||
|
|
||||||
get serverName() {
|
get serverName() {
|
||||||
|
@ -1470,9 +1474,11 @@ class Inbound extends XrayCommonClass {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let clientPassword = this.isSSMultiUser ? ':' + settings.shadowsockses[clientIndex].password : '';
|
let password = new Array();
|
||||||
|
if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password);
|
||||||
|
if (this.isSS2022) password.push(settings.password);
|
||||||
|
|
||||||
let link = `ss://${safeBase64(settings.method + ':' + settings.password + clientPassword)}@${address}:${this.port}`;
|
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${this.port}`;
|
||||||
const url = new URL(link);
|
const url = new URL(link);
|
||||||
for (const [key, value] of params) {
|
for (const [key, value] of params) {
|
||||||
url.searchParams.set(key, value)
|
url.searchParams.set(key, value)
|
||||||
|
@ -2097,8 +2103,9 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
|
||||||
};
|
};
|
||||||
|
|
||||||
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||||
constructor(password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
|
constructor(method='', password=RandomUtil.randomShadowsocksPassword(), email=RandomUtil.randomLowerAndNum(8),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId=RandomUtil.randomLowerAndNum(16)) {
|
||||||
super();
|
super();
|
||||||
|
this.method = method;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.limitIp = limitIp;
|
this.limitIp = limitIp;
|
||||||
|
@ -2111,6 +2118,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||||
|
|
||||||
toJson() {
|
toJson() {
|
||||||
return {
|
return {
|
||||||
|
method: this.method,
|
||||||
password: this.password,
|
password: this.password,
|
||||||
email: this.email,
|
email: this.email,
|
||||||
limitIp: this.limitIp,
|
limitIp: this.limitIp,
|
||||||
|
@ -2124,6 +2132,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
return new Inbound.ShadowsocksSettings.Shadowsocks(
|
return new Inbound.ShadowsocksSettings.Shadowsocks(
|
||||||
|
json.method,
|
||||||
json.password,
|
json.password,
|
||||||
json.email,
|
json.email,
|
||||||
json.limitIp,
|
json.limitIp,
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
||||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
||||||
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
||||||
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks());
|
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method));
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -115,7 +115,7 @@
|
||||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<a-form-item v-if="inbound.isSS2022" label='{{ i18n "password" }}'>
|
||||||
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||||
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
|
<a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
|
@ -110,6 +110,15 @@
|
||||||
if (this.inModal.inbound.settings.shadowsockses.length ==0){
|
if (this.inModal.inbound.settings.shadowsockses.length ==0){
|
||||||
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
||||||
}
|
}
|
||||||
|
if (["aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305"].includes(this.inModal.inbound.settings.method)) {
|
||||||
|
this.inModal.inbound.settings.shadowsockses.forEach(client => {
|
||||||
|
client.method = this.inModal.inbound.settings.method;
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.inModal.inbound.settings.shadowsockses.forEach(client => {
|
||||||
|
client.method = "";
|
||||||
|
})
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.inModal.inbound.settings.shadowsockses.length > 0){
|
if (this.inModal.inbound.settings.shadowsockses.length > 0){
|
||||||
this.inModal.inbound.settings.shadowsockses = [];
|
this.inModal.inbound.settings.shadowsockses = [];
|
||||||
|
|
|
@ -327,14 +327,15 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||||
inboundJson, err2 := json.MarshalIndent(oldInbound.GenXrayInboundConfig(), "", " ")
|
inboundJson, err2 := json.MarshalIndent(oldInbound.GenXrayInboundConfig(), "", " ")
|
||||||
if err2 != nil {
|
if err2 != nil {
|
||||||
logger.Debug("Unable to marshal updated inbound config:", err2)
|
logger.Debug("Unable to marshal updated inbound config:", err2)
|
||||||
}
|
|
||||||
|
|
||||||
err2 = s.xrayApi.AddInbound(inboundJson)
|
|
||||||
if err1 == nil {
|
|
||||||
logger.Debug("Updated inbound added by api:", oldInbound.Tag)
|
|
||||||
} else {
|
|
||||||
logger.Debug("Unable to update inbound by api:", err2)
|
|
||||||
needRestart = true
|
needRestart = true
|
||||||
|
} else {
|
||||||
|
err2 = s.xrayApi.AddInbound(inboundJson)
|
||||||
|
if err2 == nil {
|
||||||
|
logger.Debug("Updated inbound added by api:", oldInbound.Tag)
|
||||||
|
} else {
|
||||||
|
logger.Debug("Unable to update inbound by api:", err2)
|
||||||
|
needRestart = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -461,15 +462,21 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||||
if len(client.Email) > 0 {
|
if len(client.Email) > 0 {
|
||||||
s.AddClientStat(tx, data.Id, &client)
|
s.AddClientStat(tx, data.Id, &client)
|
||||||
if client.Enable {
|
if client.Enable {
|
||||||
|
cipher := ""
|
||||||
|
if oldInbound.Protocol == "shadowsocks" {
|
||||||
|
cipher = oldSettings["method"].(string)
|
||||||
|
}
|
||||||
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
|
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
|
||||||
"email": client.Email,
|
"email": client.Email,
|
||||||
"id": client.ID,
|
"id": client.ID,
|
||||||
"flow": client.Flow,
|
"flow": client.Flow,
|
||||||
"password": client.Password,
|
"password": client.Password,
|
||||||
|
"cipher": cipher,
|
||||||
})
|
})
|
||||||
if err1 == nil {
|
if err1 == nil {
|
||||||
logger.Debug("Client added by api:", client.Email)
|
logger.Debug("Client added by api:", client.Email)
|
||||||
} else {
|
} else {
|
||||||
|
logger.Debug("Error in adding client by api:", err1)
|
||||||
needRestart = true
|
needRestart = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -536,15 +543,18 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
needRestart := false
|
needRestart := false
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
|
||||||
if len(email) > 0 {
|
if len(email) > 0 {
|
||||||
err = s.xrayApi.RemoveUser(oldInbound.Tag, email)
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
if err == nil {
|
err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email)
|
||||||
|
if err1 == nil {
|
||||||
logger.Debug("Client deleted by api:", email)
|
logger.Debug("Client deleted by api:", email)
|
||||||
needRestart = false
|
needRestart = false
|
||||||
|
} else {
|
||||||
|
logger.Debug("Unable to del client by api:", err1)
|
||||||
|
needRestart = true
|
||||||
}
|
}
|
||||||
|
s.xrayApi.Close()
|
||||||
}
|
}
|
||||||
s.xrayApi.Close()
|
|
||||||
return needRestart, db.Save(oldInbound).Error
|
return needRestart, db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -650,26 +660,35 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
needRestart := false
|
needRestart := false
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
|
||||||
if len(oldEmail) > 0 {
|
if len(oldEmail) > 0 {
|
||||||
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
s.xrayApi.RemoveUser(oldInbound.Tag, oldEmail)
|
s.xrayApi.RemoveUser(oldInbound.Tag, oldEmail)
|
||||||
if clients[0].Enable {
|
if clients[0].Enable {
|
||||||
|
cipher := ""
|
||||||
|
if oldInbound.Protocol == "shadowsocks" {
|
||||||
|
cipher = oldSettings["method"].(string)
|
||||||
|
}
|
||||||
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
|
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]interface{}{
|
||||||
"email": clients[0].Email,
|
"email": clients[0].Email,
|
||||||
"id": clients[0].ID,
|
"id": clients[0].ID,
|
||||||
"flow": clients[0].Flow,
|
"flow": clients[0].Flow,
|
||||||
"password": clients[0].Password,
|
"password": clients[0].Password,
|
||||||
|
"cipher": cipher,
|
||||||
})
|
})
|
||||||
if err1 == nil {
|
if err1 == nil {
|
||||||
logger.Debug("Client edited by api:", clients[0].Email)
|
logger.Debug("Client edited by api:", clients[0].Email)
|
||||||
needRestart = false
|
} else {
|
||||||
|
logger.Debug("Error in adding client by api:", err1)
|
||||||
|
needRestart = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("Client disabled by api:", clients[0].Email)
|
logger.Debug("Client disabled by api:", clients[0].Email)
|
||||||
needRestart = false
|
|
||||||
}
|
}
|
||||||
|
s.xrayApi.Close()
|
||||||
|
} else {
|
||||||
|
logger.Debug("Client old email not found")
|
||||||
|
needRestart = true
|
||||||
}
|
}
|
||||||
s.xrayApi.Close()
|
|
||||||
return needRestart, tx.Save(oldInbound).Error
|
return needRestart, tx.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -723,6 +742,11 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid empty slice error
|
||||||
|
if len(dbClientTraffics) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)
|
dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -814,10 +838,11 @@ func (s *InboundService) DisableInvalidInbounds() (bool, int64, error) {
|
||||||
}
|
}
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
for _, tag := range tags {
|
for _, tag := range tags {
|
||||||
err = s.xrayApi.DelInbound(tag)
|
err1 := s.xrayApi.DelInbound(tag)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.Debug("Inbound disabled by api:", tag)
|
logger.Debug("Inbound disabled by api:", tag)
|
||||||
} else {
|
} else {
|
||||||
|
logger.Debug("Error in disabling inbound by api:", err1)
|
||||||
needRestart = true
|
needRestart = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -853,10 +878,11 @@ func (s *InboundService) DisableInvalidClients() (bool, int64, error) {
|
||||||
}
|
}
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
err = s.xrayApi.RemoveUser(result.Tag, result.Email)
|
err1 := s.xrayApi.RemoveUser(result.Tag, result.Email)
|
||||||
if err == nil {
|
if err1 == nil {
|
||||||
logger.Debug("Client disabled by api:", result.Email)
|
logger.Debug("Client disabled by api:", result.Email)
|
||||||
} else {
|
} else {
|
||||||
|
logger.Debug("Error in disabling client by api:", err1)
|
||||||
needRestart = true
|
needRestart = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1254,15 +1280,26 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, e
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if client.Email == clientEmail {
|
if client.Email == clientEmail {
|
||||||
s.xrayApi.Init(p.GetAPIPort())
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
|
cipher := ""
|
||||||
|
if string(inbound.Protocol) == "shadowsocks" {
|
||||||
|
var oldSettings map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(inbound.Settings), &oldSettings)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
cipher = oldSettings["method"].(string)
|
||||||
|
}
|
||||||
err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]interface{}{
|
err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]interface{}{
|
||||||
"email": client.Email,
|
"email": client.Email,
|
||||||
"id": client.ID,
|
"id": client.ID,
|
||||||
"flow": client.Flow,
|
"flow": client.Flow,
|
||||||
"password": client.Password,
|
"password": client.Password,
|
||||||
|
"cipher": cipher,
|
||||||
})
|
})
|
||||||
if err1 == nil {
|
if err1 == nil {
|
||||||
logger.Debug("Client enabled due to reset traffic:", clientEmail)
|
logger.Debug("Client enabled due to reset traffic:", clientEmail)
|
||||||
} else {
|
} else {
|
||||||
|
logger.Debug("Error in enabling client by api:", err1)
|
||||||
needRestart = true
|
needRestart = true
|
||||||
}
|
}
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
|
|
|
@ -116,7 +116,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for key := range c {
|
for key := range c {
|
||||||
if key != "email" && key != "id" && key != "password" && key != "flow" {
|
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" {
|
||||||
delete(c, key)
|
delete(c, key)
|
||||||
}
|
}
|
||||||
if c["flow"] == "xtls-rprx-vision-udp443" {
|
if c["flow"] == "xtls-rprx-vision-udp443" {
|
||||||
|
|
|
@ -63,10 +63,12 @@ func (x *XrayAPI) AddInbound(inbound []byte) error {
|
||||||
err := json.Unmarshal(inbound, conf)
|
err := json.Unmarshal(inbound, conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug("Failed to unmarshal inbound:", err)
|
logger.Debug("Failed to unmarshal inbound:", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
config, err := conf.Build()
|
config, err := conf.Build()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Debug("Failed to build inbound Detur:", err)
|
logger.Debug("Failed to build inbound Detur:", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
inboundConfig := command.AddInboundRequest{Inbound: config}
|
inboundConfig := command.AddInboundRequest{Inbound: config}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue