diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 62bc6e7c..ca2be2f1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,7 +25,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5.0.0
with:
- go-version: '1.21'
+ go-version: '1.22'
- name: Install dependencies
run: |
@@ -116,12 +116,11 @@ jobs:
- name: Package
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
- - name: Upload
- uses: svenstaro/upload-release-action@2.7.0
+ - name: Upload files to GH release
+ uses: MHSanaei/upload-release-action@2.8.0
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
file: x-ui-linux-${{ matrix.platform }}.tar.gz
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
prerelease: true
- overwrite: true
diff --git a/Dockerfile b/Dockerfile
index 5f4a36c3..010d9578 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
# ========================================================
# Stage: Builder
# ========================================================
-FROM golang:1.21-alpine AS builder
+FROM golang:1.22-alpine AS builder
WORKDIR /app
ARG TARGETARCH
diff --git a/README.md b/README.md
index a1c902e2..36764e52 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,17 @@ certbot renew --dry-run
```sh
ARCH=$(uname -m)
-[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
+case "${ARCH}" in
+ x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
+ i*86 | x86) XUI_ARCH="386" ;;
+ armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
+ armv7* | armv7) XUI_ARCH="armv7" ;;
+ armv6* | armv6) XUI_ARCH="armv6" ;;
+ armv5* | armv5) XUI_ARCH="armv5" ;;
+ *) XUI_ARCH="amd64" ;;
+esac
+
+
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
@@ -77,7 +87,16 @@ wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI
```sh
ARCH=$(uname -m)
-[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
+case "${ARCH}" in
+ x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
+ i*86 | x86) XUI_ARCH="386" ;;
+ armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
+ armv7* | armv7) XUI_ARCH="armv7" ;;
+ armv6* | armv6) XUI_ARCH="armv6" ;;
+ armv5* | armv5) XUI_ARCH="armv5" ;;
+ *) XUI_ARCH="amd64" ;;
+esac
+
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
@@ -164,22 +183,26 @@ remove 3x-ui from docker
- AlmaLinux 9+
- Rockylinux 9+
-## Compatible Architectures & Devices
+## Supported Architectures and Devices
-Supports a variety of different architectures and devices. Here are some of the main architectures that we support:
+
+ Click for Supported Architectures and devices details
-- **amd64**: This is the most common architecture for personal computers and servers. It supports most modern operating systems.
+Our platform offers compatibility with a diverse range of architectures and devices, ensuring flexibility across various computing environments. The following are key architectures that we support:
-- **x86 / i386**: This architecture is prevalent in desktop and laptop computers. It's widely supported by various operating systems and applications. (Ex: Most Windows, macOS, and Linux systems)
+- **amd64**: This prevalent architecture is the standard for personal computers and servers, accommodating most modern operating systems seamlessly.
-- **armv8 / arm64 / aarch64**: This is the architecture for modern mobile and embedded devices, including smartphones and tablets. (Ex: Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS,...)
+- **x86 / i386**: Widely adopted in desktop and laptop computers, this architecture enjoys broad support from numerous operating systems and applications, including but not limited to Windows, macOS, and Linux systems.
-- **armv7 / arm / arm32**: This is the architecture for older mobile and embedded devices. It is still widely used in many devices. (Ex: Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2,...)
+- **armv8 / arm64 / aarch64**: Tailored for contemporary mobile and embedded devices, such as smartphones and tablets, this architecture is exemplified by devices like Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, and more.
-- **armv6 / arm / arm32**: This is the architecture for very old embedded devices. While not as common as before, there are still some devices using this architecture. (Ex: Raspberry Pi 1, Raspberry Pi Zero/Zero W,...)
+- **armv7 / arm / arm32**: Serving as the architecture for older mobile and embedded devices, it remains widely utilized in devices like Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, among others.
+
+- **armv6 / arm / arm32**: Geared towards very old embedded devices, this architecture, while less prevalent, is still in use. Devices such as Raspberry Pi 1, Raspberry Pi Zero/Zero W, rely on this architecture.
+
+- **armv5 / arm / arm32**: An older architecture primarily associated with early embedded systems, it is less common today but may still be found in legacy devices like early Raspberry Pi versions and some older smartphones.
+
-- **armv5 / arm / arm32**: This is an older architecture primarily used in early embedded systems. While it's less common today, some legacy devices may still rely on this architecture. (Ex: Early versions of Raspberry Pi, some older smartphones)
-
## Languages
- English
@@ -188,6 +211,7 @@ Supports a variety of different architectures and devices. Here are some of the
- Russian
- Vietnamese
- Spanish
+- Indonesian
## Features
diff --git a/go.mod b/go.mod
index 722df287..0d274bcb 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module x-ui
-go 1.21.4
+go 1.22.0
require (
github.com/Calidity/gin-sessions v1.3.1
diff --git a/sub/default.json b/sub/default.json
new file mode 100644
index 00000000..ba13f6fb
--- /dev/null
+++ b/sub/default.json
@@ -0,0 +1,105 @@
+{
+ "dns": {
+ "tag": "dns_out",
+ "queryStrategy": "UseIP",
+ "servers": [
+ {
+ "address": "8.8.8.8",
+ "skipFallback": false
+ }
+ ]
+ },
+ "inbounds": [
+ {
+ "port": 10808,
+ "protocol": "socks",
+ "settings": {
+ "auth": "noauth",
+ "udp": true,
+ "userLevel": 8
+ },
+ "sniffing": {
+ "destOverride": [
+ "http",
+ "tls",
+ "fakedns"
+ ],
+ "enabled": true
+ },
+ "tag": "socks"
+ },
+ {
+ "port": 10809,
+ "protocol": "http",
+ "settings": {
+ "userLevel": 8
+ },
+ "tag": "http"
+ }
+ ],
+ "log": {
+ "loglevel": "warning"
+ },
+ "outbounds": [
+ {
+ "tag": "direct",
+ "protocol": "freedom",
+ "settings": {
+ "domainStrategy": "UseIP"
+ }
+ },
+ {
+ "tag": "block",
+ "protocol": "blackhole",
+ "settings": {
+ "response": {
+ "type": "http"
+ }
+ }
+ }
+ ],
+ "policy": {
+ "levels": {
+ "8": {
+ "connIdle": 300,
+ "downlinkOnly": 1,
+ "handshake": 4,
+ "uplinkOnly": 1
+ }
+ },
+ "system": {
+ "statsOutboundUplink": true,
+ "statsOutboundDownlink": true
+ }
+ },
+ "routing": {
+ "domainStrategy": "AsIs",
+ "rules": [
+ {
+ "type": "field",
+ "network": "tcp,udp",
+ "balancerTag": "all"
+ }
+ ],
+ "balancers": [
+ {
+ "tag": "all",
+ "selector": [
+ "proxy"
+ ],
+ "strategy": {
+ "type": "leastPing"
+ }
+ }
+ ]
+ },
+ "observatory": {
+ "probeInterval": "5m",
+ "probeURL": "https://api.github.com/_private/browser/stats",
+ "subjectSelector": [
+ "proxy"
+ ],
+ "EnableConcurrency": true
+ },
+ "stats": {}
+}
\ No newline at end of file
diff --git a/sub/sub.go b/sub/sub.go
index b642f7f2..2a4a37f4 100644
--- a/sub/sub.go
+++ b/sub/sub.go
@@ -47,11 +47,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default()
- subPath, err := s.settingService.GetSubPath()
- if err != nil {
- return nil, err
- }
-
subDomain, err := s.settingService.GetSubDomain()
if err != nil {
return nil, err
@@ -61,9 +56,44 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
}
- g := engine.Group(subPath)
+ LinksPath, err := s.settingService.GetSubPath()
+ if err != nil {
+ return nil, err
+ }
- s.sub = NewSUBController(g)
+ JsonPath, err := s.settingService.GetSubJsonPath()
+ if err != nil {
+ return nil, err
+ }
+
+ Encrypt, err := s.settingService.GetSubEncrypt()
+ if err != nil {
+ return nil, err
+ }
+
+ ShowInfo, err := s.settingService.GetSubShowInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ RemarkModel, err := s.settingService.GetRemarkModel()
+ if err != nil {
+ RemarkModel = "-ieo"
+ }
+
+ SubUpdates, err := s.settingService.GetSubUpdates()
+ if err != nil {
+ SubUpdates = "10"
+ }
+
+ SubJsonFragment, err := s.settingService.GetSubJsonFragment()
+ if err != nil {
+ SubJsonFragment = ""
+ }
+
+ g := engine.Group("/")
+
+ s.sub = NewSUBController(g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment)
return engine, nil
}
diff --git a/sub/subController.go b/sub/subController.go
index 5f7c69cf..e0b641df 100644
--- a/sub/subController.go
+++ b/sub/subController.go
@@ -3,34 +3,57 @@ package sub
import (
"encoding/base64"
"strings"
- "x-ui/web/service"
"github.com/gin-gonic/gin"
)
type SUBController struct {
- subService SubService
- settingService service.SettingService
+ subPath string
+ subJsonPath string
+ subEncrypt bool
+ updateInterval string
+
+ subService *SubService
+ subJsonService *SubJsonService
}
-func NewSUBController(g *gin.RouterGroup) *SUBController {
- a := &SUBController{}
+func NewSUBController(
+ g *gin.RouterGroup,
+ subPath string,
+ jsonPath string,
+ encrypt bool,
+ showInfo bool,
+ rModel string,
+ update string,
+ jsonFragment string) *SUBController {
+
+ a := &SUBController{
+ subPath: subPath,
+ subJsonPath: jsonPath,
+ subEncrypt: encrypt,
+ updateInterval: update,
+
+ subService: NewSubService(showInfo, rModel),
+ subJsonService: NewSubJsonService(jsonFragment),
+ }
a.initRouter(g)
return a
}
func (a *SUBController) initRouter(g *gin.RouterGroup) {
- g = g.Group("/")
+ gLink := g.Group(a.subPath)
+ gJson := g.Group(a.subJsonPath)
- g.GET("/:subid", a.subs)
+ gLink.GET(":subid", a.subs)
+
+ gJson.GET(":subid", a.subJsons)
}
func (a *SUBController) subs(c *gin.Context) {
- subEncrypt, _ := a.settingService.GetSubEncrypt()
- subShowInfo, _ := a.settingService.GetSubShowInfo()
+ println(c.Request.Header["User-Agent"][0])
subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0]
- subs, headers, err := a.subService.GetSubs(subId, host, subShowInfo)
+ subs, header, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 {
c.String(400, "Error!")
} else {
@@ -40,14 +63,32 @@ func (a *SUBController) subs(c *gin.Context) {
}
// Add headers
- c.Writer.Header().Set("Subscription-Userinfo", headers[0])
- c.Writer.Header().Set("Profile-Update-Interval", headers[1])
- c.Writer.Header().Set("Profile-Title", headers[2])
+ c.Writer.Header().Set("Subscription-Userinfo", header)
+ c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
+ c.Writer.Header().Set("Profile-Title", subId)
- if subEncrypt {
+ if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} else {
c.String(200, result)
}
}
}
+
+func (a *SUBController) subJsons(c *gin.Context) {
+ println(c.Request.Header["User-Agent"][0])
+ subId := c.Param("subid")
+ host := strings.Split(c.Request.Host, ":")[0]
+ jsonSub, header, err := a.subJsonService.GetJson(subId, host)
+ if err != nil || len(jsonSub) == 0 {
+ c.String(400, "Error!")
+ } else {
+
+ // Add headers
+ c.Writer.Header().Set("Subscription-Userinfo", header)
+ c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
+ c.Writer.Header().Set("Profile-Title", subId)
+
+ c.String(200, jsonSub)
+ }
+}
diff --git a/sub/subJsonService.go b/sub/subJsonService.go
new file mode 100644
index 00000000..92519f3e
--- /dev/null
+++ b/sub/subJsonService.go
@@ -0,0 +1,355 @@
+package sub
+
+import (
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "x-ui/database/model"
+ "x-ui/logger"
+ "x-ui/util/json_util"
+ "x-ui/util/random"
+ "x-ui/web/service"
+ "x-ui/xray"
+)
+
+//go:embed default.json
+var defaultJson string
+
+type SubJsonService struct {
+ fragmanet string
+
+ inboundService service.InboundService
+ SubService
+}
+
+func NewSubJsonService(fragment string) *SubJsonService {
+ return &SubJsonService{
+ fragmanet: fragment,
+ }
+}
+
+func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
+ inbounds, err := s.SubService.getInboundsBySubId(subId)
+ if err != nil || len(inbounds) == 0 {
+ return "", "", err
+ }
+
+ var header string
+ var traffic xray.ClientTraffic
+ var clientTraffics []xray.ClientTraffic
+ var configJson map[string]interface{}
+ var defaultOutbounds []json_util.RawMessage
+
+ json.Unmarshal([]byte(defaultJson), &configJson)
+ if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok {
+ for _, defaultOutbound := range outboundSlices {
+ jsonBytes, _ := json.Marshal(defaultOutbound)
+ defaultOutbounds = append(defaultOutbounds, jsonBytes)
+ }
+ }
+
+ outbounds := []json_util.RawMessage{}
+ startIndex := 0
+ // Prepare Inbounds
+ for _, inbound := range inbounds {
+ clients, err := s.inboundService.GetClients(inbound)
+ if err != nil {
+ logger.Error("SubJsonService - GetClients: Unable to get clients from inbound")
+ }
+ if clients == nil {
+ continue
+ }
+ if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
+ listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
+ if err == nil {
+ inbound.Listen = listen
+ inbound.Port = port
+ inbound.StreamSettings = streamSettings
+ }
+ }
+
+ var subClients []model.Client
+ for _, client := range clients {
+ if client.Enable && client.SubID == subId {
+ subClients = append(subClients, client)
+ clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
+ }
+ }
+
+ outbound := s.getOutbound(inbound, subClients, host, startIndex)
+ if outbound != nil {
+ outbounds = append(outbounds, outbound...)
+ startIndex += len(outbound)
+ }
+ }
+
+ if len(outbounds) == 0 {
+ return "", "", nil
+ }
+
+ // Prepare statistics
+ for index, clientTraffic := range clientTraffics {
+ if index == 0 {
+ traffic.Up = clientTraffic.Up
+ traffic.Down = clientTraffic.Down
+ traffic.Total = clientTraffic.Total
+ if clientTraffic.ExpiryTime > 0 {
+ traffic.ExpiryTime = clientTraffic.ExpiryTime
+ }
+ } else {
+ traffic.Up += clientTraffic.Up
+ traffic.Down += clientTraffic.Down
+ if traffic.Total == 0 || clientTraffic.Total == 0 {
+ traffic.Total = 0
+ } else {
+ traffic.Total += clientTraffic.Total
+ }
+ if clientTraffic.ExpiryTime != traffic.ExpiryTime {
+ traffic.ExpiryTime = 0
+ }
+ }
+ }
+
+ if s.fragmanet != "" {
+ outbounds = append(outbounds, json_util.RawMessage(s.fragmanet))
+ }
+
+ // Combile outbounds
+ outbounds = append(outbounds, defaultOutbounds...)
+ configJson["outbounds"] = outbounds
+ finalJson, _ := json.MarshalIndent(configJson, "", " ")
+
+ header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
+ return string(finalJson), header, nil
+}
+
+func (s *SubJsonService) getOutbound(inbound *model.Inbound, clients []model.Client, host string, startIndex int) []json_util.RawMessage {
+ var newOutbounds []json_util.RawMessage
+ stream := s.streamData(inbound.StreamSettings)
+
+ externalProxies, ok := stream["externalProxy"].([]interface{})
+ if !ok || len(externalProxies) == 0 {
+ externalProxies = []interface{}{
+ map[string]interface{}{
+ "forceTls": "same",
+ "dest": host,
+ "port": float64(inbound.Port),
+ },
+ }
+ }
+
+ delete(stream, "externalProxy")
+
+ config_index := startIndex
+ for _, ep := range externalProxies {
+ extPrxy := ep.(map[string]interface{})
+ inbound.Listen = extPrxy["dest"].(string)
+ inbound.Port = int(extPrxy["port"].(float64))
+ newStream := stream
+ switch extPrxy["forceTls"].(string) {
+ case "tls":
+ if newStream["security"] != "tls" {
+ newStream["security"] = "tls"
+ newStream["tslSettings"] = map[string]interface{}{}
+ }
+ case "none":
+ if newStream["security"] != "none" {
+ newStream["security"] = "none"
+ delete(newStream, "tslSettings")
+ }
+ }
+ streamSettings, _ := json.MarshalIndent(newStream, "", " ")
+ inbound.StreamSettings = string(streamSettings)
+
+ for _, client := range clients {
+ inbound.Tag = fmt.Sprintf("proxy_%d", config_index)
+ switch inbound.Protocol {
+ case "vmess", "vless":
+ newOutbounds = append(newOutbounds, s.genVnext(inbound, client))
+ case "trojan", "shadowsocks":
+ newOutbounds = append(newOutbounds, s.genServer(inbound, client))
+ }
+ config_index += 1
+ }
+ }
+
+ return newOutbounds
+}
+
+func (s *SubJsonService) streamData(stream string) map[string]interface{} {
+ var streamSettings map[string]interface{}
+ json.Unmarshal([]byte(stream), &streamSettings)
+ security, _ := streamSettings["security"].(string)
+ if security == "tls" {
+ streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]interface{}))
+ } else if security == "reality" {
+ streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]interface{}))
+ }
+ delete(streamSettings, "sockopt")
+
+ if s.fragmanet != "" {
+ streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "TcpNoDelay": true}`)
+ }
+
+ // remove proxy protocol
+ network, _ := streamSettings["network"].(string)
+ switch network {
+ case "tcp":
+ streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"])
+ case "ws":
+ streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"])
+ }
+
+ return streamSettings
+}
+
+func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]interface{} {
+ netSettings, ok := setting.(map[string]interface{})
+ if ok {
+ delete(netSettings, "acceptProxyProtocol")
+ }
+ return netSettings
+}
+
+func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interface{} {
+ tlsData := make(map[string]interface{}, 1)
+ tlsClientSettings := tData["settings"].(map[string]interface{})
+
+ tlsData["serverName"] = tData["serverName"]
+ tlsData["alpn"] = tData["alpn"]
+ if allowInsecure, ok := tlsClientSettings["allowInsecure"].(string); ok {
+ tlsData["allowInsecure"] = allowInsecure
+ }
+ if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
+ tlsData["fingerprint"] = fingerprint
+ }
+ return tlsData
+}
+
+func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]interface{} {
+ rltyData := make(map[string]interface{}, 1)
+ rltyClientSettings := rData["settings"].(map[string]interface{})
+
+ rltyData["show"] = false
+ rltyData["publicKey"] = rltyClientSettings["publicKey"]
+ rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
+
+ // Set random data
+ rltyData["spiderX"] = "/" + random.Seq(15)
+ shortIds, ok := rData["shortIds"].([]interface{})
+ if ok && len(shortIds) > 0 {
+ rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
+ } else {
+ rltyData["shortId"] = ""
+ }
+ serverNames, ok := rData["serverNames"].([]interface{})
+ if ok && len(serverNames) > 0 {
+ rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
+ } else {
+ rltyData["serverName"] = ""
+ }
+
+ return rltyData
+}
+
+func (s *SubJsonService) genVnext(inbound *model.Inbound, client model.Client) json_util.RawMessage {
+ outbound := Outbound{}
+ usersData := make([]UserVnext, 1)
+
+ usersData[0].ID = client.ID
+ usersData[0].Level = 8
+ if inbound.Protocol == model.VLESS {
+ usersData[0].Flow = client.Flow
+ usersData[0].Encryption = "none"
+ }
+
+ vnextData := make([]VnextSetting, 1)
+ vnextData[0] = VnextSetting{
+ Address: inbound.Listen,
+ Port: inbound.Port,
+ Users: usersData,
+ }
+
+ outbound.Protocol = string(inbound.Protocol)
+ outbound.Tag = inbound.Tag
+ outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
+ outbound.Settings = OutboundSettings{
+ Vnext: vnextData,
+ }
+
+ result, _ := json.MarshalIndent(outbound, "", " ")
+ return result
+}
+
+func (s *SubJsonService) genServer(inbound *model.Inbound, client model.Client) json_util.RawMessage {
+ outbound := Outbound{}
+
+ serverData := make([]ServerSetting, 1)
+ serverData[0] = ServerSetting{
+ Address: inbound.Listen,
+ Port: inbound.Port,
+ Level: 8,
+ Password: client.Password,
+ }
+
+ if inbound.Protocol == model.Shadowsocks {
+ var inboundSettings map[string]interface{}
+ json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
+ method, _ := inboundSettings["method"].(string)
+ serverData[0].Method = method
+
+ // server password in multi-user 2022 protocols
+ if strings.HasPrefix(method, "2022") {
+ if serverPassword, ok := inboundSettings["password"].(string); ok {
+ serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password)
+ }
+ }
+ }
+
+ outbound.Protocol = string(inbound.Protocol)
+ outbound.Tag = inbound.Tag
+ outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
+ outbound.Settings = OutboundSettings{
+ Servers: serverData,
+ }
+
+ result, _ := json.MarshalIndent(outbound, "", " ")
+ return result
+}
+
+type Outbound struct {
+ Protocol string `json:"protocol"`
+ Tag string `json:"tag"`
+ StreamSettings json_util.RawMessage `json:"streamSettings"`
+ Mux map[string]interface{} `json:"mux,omitempty"`
+ ProxySettings map[string]interface{} `json:"proxySettings,omitempty"`
+ Settings OutboundSettings `json:"settings,omitempty"`
+}
+
+type OutboundSettings struct {
+ Vnext []VnextSetting `json:"vnext,omitempty"`
+ Servers []ServerSetting `json:"servers,omitempty"`
+}
+
+type VnextSetting struct {
+ Address string `json:"address"`
+ Port int `json:"port"`
+ Users []UserVnext `json:"users"`
+}
+
+type UserVnext struct {
+ Encryption string `json:"encryption,omitempty"`
+ Flow string `json:"flow,omitempty"`
+ ID string `json:"id"`
+ Level int `json:"level"`
+}
+
+type ServerSetting struct {
+ Password string `json:"password"`
+ Level int `json:"level"`
+ Address string `json:"address"`
+ Port int `json:"port"`
+ Flow string `json:"flow,omitempty"`
+ Method string `json:"method,omitempty"`
+}
diff --git a/sub/subService.go b/sub/subService.go
index ddf9692b..06d1ed0a 100644
--- a/sub/subService.go
+++ b/sub/subService.go
@@ -25,47 +25,42 @@ type SubService struct {
settingService service.SettingService
}
-func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
+func NewSubService(showInfo bool, remarkModel string) *SubService {
+ return &SubService{
+ showInfo: showInfo,
+ remarkModel: remarkModel,
+ }
+}
+
+func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
s.address = host
- s.showInfo = showInfo
var result []string
- var headers []string
+ var header string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
- return nil, nil, err
- }
- s.remarkModel, err = s.settingService.GetRemarkModel()
- if err != nil {
- s.remarkModel = "-ieo"
+ return nil, "", err
}
+
s.datepicker, err = s.settingService.GetDatepicker()
- if err != nil {
- s.datepicker = "gregorian"
- }
+ if err != nil {
+ s.datepicker = "gregorian"
+ }
for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
- logger.Error("SubService - GetSub: Unable to get clients from inbound")
+ logger.Error("SubService - GetClients: Unable to get clients from inbound")
}
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
- fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
+ listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
if err == nil {
- inbound.Listen = fallbackMaster.Listen
- inbound.Port = fallbackMaster.Port
- var stream map[string]interface{}
- json.Unmarshal([]byte(inbound.StreamSettings), &stream)
- var masterStream map[string]interface{}
- json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
- stream["security"] = masterStream["security"]
- stream["tlsSettings"] = masterStream["tlsSettings"]
- stream["externalProxy"] = masterStream["externalProxy"]
- modifiedStream, _ := json.MarshalIndent(stream, "", " ")
- inbound.StreamSettings = string(modifiedStream)
+ inbound.Listen = listen
+ inbound.Port = port
+ inbound.StreamSettings = streamSettings
}
}
for _, client := range clients {
@@ -76,6 +71,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
}
}
}
+
+ // Prepare statistics
for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
@@ -97,11 +94,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
}
}
}
- headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
- updateInterval, _ := s.settingService.GetSubUpdates()
- headers = append(headers, fmt.Sprintf("%d", updateInterval))
- headers = append(headers, subId)
- return result, headers, nil
+ header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
+ return result, header, nil
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
@@ -130,7 +124,7 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
return xray.ClientTraffic{}
}
-func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
+func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
db := database.GetDB()
var inbound *model.Inbound
err := db.Model(model.Inbound{}).
@@ -138,9 +132,19 @@ func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error
if err != nil {
- return nil, err
+ return "", 0, "", err
}
- return inbound, nil
+
+ var stream map[string]interface{}
+ json.Unmarshal([]byte(streamSettings), &stream)
+ var masterStream map[string]interface{}
+ json.Unmarshal([]byte(inbound.StreamSettings), &masterStream)
+ stream["security"] = masterStream["security"]
+ stream["tlsSettings"] = masterStream["tlsSettings"]
+ stream["externalProxy"] = masterStream["externalProxy"]
+ modifiedStream, _ := json.MarshalIndent(stream, "", " ")
+
+ return inbound.Listen, inbound.Port, string(modifiedStream), nil
}
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
@@ -578,6 +582,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
+
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
diff --git a/web/assets/css/custom.css b/web/assets/css/custom.css
index c41bcc16..8eb5037f 100644
--- a/web/assets/css/custom.css
+++ b/web/assets/css/custom.css
@@ -1050,6 +1050,12 @@ li.ant-select-dropdown-menu-item:empty:after {
color: rgba(255, 255, 255, 0.25);
}
+.dark .ant-message-notice-content {
+ background-color: #222d42;
+ border: 1px solid #2c3950;
+ color: rgba(255, 255, 255, 0.65);
+}
+
.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(
:last-child
),
diff --git a/web/assets/favicon.ico b/web/assets/favicon.ico
deleted file mode 100644
index 99b108ab..00000000
Binary files a/web/assets/favicon.ico and /dev/null differ
diff --git a/web/assets/js/model/setting.js b/web/assets/js/model/setting.js
index ddf1a0e1..637830e8 100644
--- a/web/assets/js/model/setting.js
+++ b/web/assets/js/model/setting.js
@@ -28,6 +28,7 @@ class AllSetting {
this.subListen = "";
this.subPort = "2096";
this.subPath = "/sub/";
+ this.subJsonPath = "/json/";
this.subDomain = "";
this.subCertFile = "";
this.subKeyFile = "";
@@ -35,6 +36,8 @@ class AllSetting {
this.subEncrypt = true;
this.subShowInfo = false;
this.subURI = '';
+ this.subJsonURI = '';
+ this.subJsonFragment = '';
this.timeLocation = "Asia/Tehran";
diff --git a/web/assets/js/model/xray.js b/web/assets/js/model/xray.js
index d7d1fa0d..791e8533 100644
--- a/web/assets/js/model/xray.js
+++ b/web/assets/js/model/xray.js
@@ -1146,10 +1146,6 @@ class Inbound extends XrayCommonClass {
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
}
- canSniffing() {
- return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
- }
-
reset() {
this.port = RandomUtil.randomIntRange(10000, 60000);
this.listen = '';
@@ -2299,7 +2295,7 @@ Inbound.WireguardSettings = class extends XrayCommonClass {
}
addPeer() {
- this.peers.push(new Inbound.WireguardSettings.Peer());
+ this.peers.push(new Inbound.WireguardSettings.Peer(null,null,'',['10.0.0.' + (this.peers.length+2)]));
}
delPeer(index) {
@@ -2327,7 +2323,7 @@ Inbound.WireguardSettings = class extends XrayCommonClass {
};
Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
- constructor(privateKey, publicKey, psk='', allowedIPs=['10.0.0.0/24'], keepAlive=0) {
+ constructor(privateKey, publicKey, psk='', allowedIPs=['10.0.0.2/32'], keepAlive=0) {
super();
this.privateKey = privateKey
this.publicKey = publicKey;
@@ -2335,6 +2331,9 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
[this.publicKey, this.privateKey] = Object.values(Wireguard.generateKeypair())
}
this.psk = psk;
+ allowedIPs.forEach((a,index) => {
+ if (a.length>0 && !a.includes('/')) allowedIPs[index] += '/32';
+ })
this.allowedIPs = allowedIPs;
this.keepAlive = keepAlive;
}
@@ -2350,6 +2349,9 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
}
toJson() {
+ this.allowedIPs.forEach((a,index) => {
+ if (a.length>0 && !a.includes('/')) this.allowedIPs[index] += '/32';
+ });
return {
privateKey: this.privateKey,
publicKey: this.publicKey,
diff --git a/web/controller/xray_setting.go b/web/controller/xray_setting.go
index 28f55b54..2dddb44b 100644
--- a/web/controller/xray_setting.go
+++ b/web/controller/xray_setting.go
@@ -81,7 +81,6 @@ func (a *XraySettingController) warp(c *gin.Context) {
resp, err = a.XraySettingService.RegWarp(skey, pkey)
case "license":
license := c.PostForm("license")
- println(license)
resp, err = a.XraySettingService.SetWarpLicence(license)
}
diff --git a/web/entity/entity.go b/web/entity/entity.go
index 8ab06399..06850128 100644
--- a/web/entity/entity.go
+++ b/web/entity/entity.go
@@ -48,6 +48,9 @@ type AllSetting struct {
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
SubURI string `json:"subURI" form:"subURI"`
+ SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
+ SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
+ SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
Datepicker string `json:"datepicker" form:"datepicker"`
}
@@ -105,6 +108,13 @@ func (s *AllSetting) CheckValid() error {
s.SubPath += "/"
}
+ if !strings.HasPrefix(s.SubJsonPath, "/") {
+ s.SubJsonPath = "/" + s.SubJsonPath
+ }
+ if !strings.HasSuffix(s.SubJsonPath, "/") {
+ s.SubJsonPath += "/"
+ }
+
_, err := time.LoadLocation(s.TimeLocation)
if err != nil {
return common.NewError("time location not exist:", s.TimeLocation)
diff --git a/web/html/common/head.html b/web/html/common/head.html
index e20cdc24..9d7919b6 100644
--- a/web/html/common/head.html
+++ b/web/html/common/head.html
@@ -7,8 +7,6 @@
-
-
-{{end}}
+{{end}}
\ No newline at end of file
diff --git a/web/html/common/text_modal.html b/web/html/common/text_modal.html
index 4fe2f175..68387be2 100644
--- a/web/html/common/text_modal.html
+++ b/web/html/common/text_modal.html
@@ -1,15 +1,16 @@
{{define "textModal"}}
-
- {{ i18n "download" }} [[ txtModal.fileName ]]
-
+ :closable="true"
+ :class="themeSwitcher.currentTheme">
+
+ [[ txtModal.fileName ]]
+
+ {{ i18n "copy" }}
+
+ :autosize="{ minRows: 10, maxRows: 20}">
{{end}}
\ No newline at end of file
diff --git a/web/html/xui/dns_modal.html b/web/html/xui/dns_modal.html
new file mode 100644
index 00000000..8b687ab2
--- /dev/null
+++ b/web/html/xui/dns_modal.html
@@ -0,0 +1,86 @@
+{{define "dnsModal"}}
+
+
+
+
+
+
+ +
+
+
+ -
+
+
+
+
+
+
+ [[ l ]]
+
+
+
+
+
+
+{{end}}
diff --git a/web/html/xui/fakedns_modal.html b/web/html/xui/fakedns_modal.html
new file mode 100644
index 00000000..c830f44d
--- /dev/null
+++ b/web/html/xui/fakedns_modal.html
@@ -0,0 +1,57 @@
+{{define "fakednsModal"}}
+
+
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/web/html/xui/form/inbound.html b/web/html/xui/form/inbound.html
index 6f3705ff..9453f0d7 100644
--- a/web/html/xui/form/inbound.html
+++ b/web/html/xui/form/inbound.html
@@ -114,7 +114,7 @@
-
+
{{template "form/sniffing"}}
{{end}}
diff --git a/web/html/xui/form/sniffing.html b/web/html/xui/form/sniffing.html
index a088dee7..d4f55394 100644
--- a/web/html/xui/form/sniffing.html
+++ b/web/html/xui/form/sniffing.html
@@ -1,6 +1,6 @@
{{define "form/sniffing"}}
-
+
Sniffing
diff --git a/web/html/xui/inbound_client_table.html b/web/html/xui/inbound_client_table.html
index d7619dde..c4c405ec 100644
--- a/web/html/xui/inbound_client_table.html
+++ b/web/html/xui/inbound_client_table.html
@@ -40,7 +40,7 @@
-
+
{{ i18n "online" }}
@@ -52,7 +52,7 @@
{{ i18n "depleted" }}
{{ i18n "disabled" }}
- {{ i18n "online" }}
+ {{ i18n "online" }}
diff --git a/web/html/xui/inbound_info_modal.html b/web/html/xui/inbound_info_modal.html
index 23f8bd47..c8341651 100644
--- a/web/html/xui/inbound_info_modal.html
+++ b/web/html/xui/inbound_info_modal.html
@@ -166,7 +166,7 @@
Subscription URL
- [[ infoModal.subLink ]]
+ SUB: [[ infoModal.subLink ]]
+
+ JSON: [[ infoModal.subJsonLink ]]
+
+
+
+
+
+
Telegram ID
@@ -345,6 +355,7 @@
index: null,
isExpired: false,
subLink: '',
+ subJsonLink: '',
show(dbInbound, index) {
this.index = index;
this.inbound = dbInbound.toInbound();
@@ -360,6 +371,7 @@
if (this.clientSettings) {
if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId);
+ this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
}
}
this.visible = true;
@@ -369,6 +381,9 @@
},
genSubLink(subID) {
return app.subSettings.subURI+subID;
+ },
+ genSubJsonLink(subID) {
+ return app.subSettings.subJsonURI+subID;
}
};
diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html
index ab42e09c..fa89fada 100644
--- a/web/html/xui/inbound_modal.html
+++ b/web/html/xui/inbound_modal.html
@@ -40,7 +40,7 @@
inModal.visible = false;
inModal.loading(false);
},
- loading(loading) {
+ loading(loading=true) {
inModal.confirmLoading = loading;
},
};
diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html
index 93f25730..a2365cde 100644
--- a/web/html/xui/inbounds.html
+++ b/web/html/xui/inbounds.html
@@ -56,9 +56,13 @@
-
- Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information
-
+
+
@@ -133,7 +137,7 @@
{{ i18n "pages.inbounds.export" }}
-
+
{{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }}
@@ -221,7 +225,7 @@
{{ i18n "pages.inbounds.export"}}
-
+
{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}
@@ -567,11 +571,13 @@
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
subSettings: {
enable : false,
- subURI : ''
+ subURI : '',
+ subJsonURI : '',
},
remarkModel: '-ieo',
datepicker: 'gregorian',
tgBotEnable: false,
+ showAlert: false,
pageSize: 0,
isMobile: window.innerWidth <= 768,
},
@@ -613,7 +619,8 @@
this.tgBotEnable = tgBotEnable;
this.subSettings = {
enable : subEnable,
- subURI: subURI
+ subURI: subURI,
+ subJsonURI: subJsonURI
};
this.pageSize = pageSize;
this.remarkModel = remarkModel;
@@ -651,9 +658,9 @@
clientCount = clients.length;
if (dbInbound.enable) {
clients.forEach(client => {
- if (client.enable && this.isClientOnline(client.email)) {
+ if (client.enable) {
active.push(client.email);
- online.push(client.email);
+ if (this.isClientOnline(client.email)) online.push(client.email);
} else {
deactive.push(client.email);
}
@@ -831,7 +838,7 @@
protocol: baseInbound.protocol,
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(),
- sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
+ sniffing: baseInbound.sniffing.toString(),
};
await this.submit('/panel/inbound/add', data, inModal);
},
@@ -880,7 +887,7 @@
settings: inbound.settings.toString(),
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
- if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
+ data.sniffing = inbound.sniffing.toString();
await this.submit('/panel/inbound/add', data, inModal);
},
@@ -899,7 +906,7 @@
settings: inbound.settings.toString(),
};
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
- if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
+ data.sniffing = inbound.sniffing.toString();
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
},
@@ -987,7 +994,7 @@
},
delInbound(dbInboundId) {
this.$confirm({
- title: '{{ i18n "pages.inbounds.deleteInbound"}}',
+ title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}',
@@ -1000,7 +1007,7 @@
clientId = this.getClientId(dbInbound.protocol, client);
if (confirmation){
this.$confirm({
- title: '{{ i18n "pages.inbounds.deleteClient"}}',
+ title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}',
@@ -1291,7 +1298,7 @@
pagination(obj){
if (this.pageSize > 0 && obj.length>this.pageSize) {
// Set page options based on object size
- sizeOptions = []
+ sizeOptions = [];
for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
sizeOptions.push(i.toString());
}
@@ -1304,8 +1311,8 @@
position: 'bottom',
pageSize: this.pageSize,
pageSizeOptions: sizeOptions
- }
- return p
+ };
+ return p;
}
return false
},
@@ -1319,6 +1326,9 @@
}, 500)
},
mounted() {
+ if (window.location.protocol !== "https:") {
+ this.showAlert = true;
+ }
window.addEventListener('resize', this.onResize);
this.onResize();
this.loading();
@@ -1356,7 +1366,6 @@
}
},
});
-
{{template "inboundModal"}}
@@ -1366,6 +1375,5 @@
{{template "inboundInfoModal"}}
{{template "clientsModal"}}
{{template "clientsBulkModal"}}
-