浏览代码

0.3.0

 - 增加到期时间限制
 - 新增配置面板 https 访问后,http 自动跳转 https(同端口)
 - 降低获取系统连接数的 cpu 使用率
 - 优化界面
 - VMess 协议 alterId 默认改为 0
 - 修复旧版本 iOS 系统白屏问题
 - 修复重启面板后 xray 没有启动的问题
sprov 4 年之前
父节点
当前提交
292d5b89d4

+ 1 - 1
config/version

@@ -1 +1 @@
-0.2.0
+0.3.0

+ 8 - 2
logger/logger.go

@@ -7,16 +7,22 @@ import (
 
 var logger *logging.Logger
 
+func init() {
+	InitLogger(logging.INFO)
+}
+
 func InitLogger(level logging.Level) {
 	format := logging.MustStringFormatter(
 		`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
 	)
-	logger = logging.MustGetLogger("x-ui")
+	newLogger := logging.MustGetLogger("x-ui")
 	backend := logging.NewLogBackend(os.Stderr, "", 0)
 	backendFormatter := logging.NewBackendFormatter(backend, format)
 	backendLeveled := logging.AddModuleLevel(backendFormatter)
 	backendLeveled.SetLevel(level, "")
-	logger.SetBackend(backendLeveled)
+	newLogger.SetBackend(backendLeveled)
+
+	logger = newLogger
 }
 
 func Debug(args ...interface{}) {

+ 0 - 0
util/sys/a.s


+ 8 - 0
util/sys/psutil.go

@@ -0,0 +1,8 @@
+package sys
+
+import (
+	_ "unsafe"
+)
+
+//go:linkname HostProc github.com/shirou/gopsutil/internal/common.HostProc
+func HostProc(combineWith ...string) string

+ 23 - 0
util/sys/sys_darwin.go

@@ -0,0 +1,23 @@
+// +build darwin
+
+package sys
+
+import (
+	"github.com/shirou/gopsutil/net"
+)
+
+func GetTCPCount() (int, error) {
+	stats, err := net.Connections("tcp")
+	if err != nil {
+		return 0, err
+	}
+	return len(stats), nil
+}
+
+func GetUDPCount() (int, error) {
+	stats, err := net.Connections("udp")
+	if err != nil {
+		return 0, err
+	}
+	return len(stats), nil
+}

+ 70 - 0
util/sys/sys_linux.go

@@ -0,0 +1,70 @@
+// +build linux
+
+package sys
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"os"
+)
+
+func getLinesNum(filename string) (int, error) {
+	file, err := os.Open(filename)
+	if err != nil {
+		return 0, err
+	}
+	defer file.Close()
+
+	sum := 0
+	buf := make([]byte, 8192)
+	for {
+		n, err := file.Read(buf)
+
+		var buffPosition int
+		for {
+			i := bytes.IndexByte(buf[buffPosition:], '\n')
+			if i < 0 || n == buffPosition {
+				break
+			}
+			buffPosition += i + 1
+			sum++
+		}
+
+		if err == io.EOF {
+			return sum, nil
+		} else if err != nil {
+			return sum, err
+		}
+	}
+}
+
+func GetTCPCount() (int, error) {
+	root := HostProc()
+
+	tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
+	if err != nil {
+		return tcp4, err
+	}
+	tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
+	if err != nil {
+		return tcp4 + tcp6, nil
+	}
+
+	return tcp4 + tcp6, nil
+}
+
+func GetUDPCount() (int, error) {
+	root := HostProc()
+
+	udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
+	if err != nil {
+		return udp4, err
+	}
+	udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
+	if err != nil {
+		return udp4 + udp6, nil
+	}
+
+	return udp4 + udp6, nil
+}

+ 54 - 29
web/assets/js/model/models.js

@@ -1,14 +1,18 @@
 class User {
-    username = "";
-    password = "";
+
+    constructor() {
+        this.username = "";
+        this.password = "";
+    }
 }
 
 class Msg {
-    success = false;
-    msg = "";
-    obj = null;
 
     constructor(success, msg, obj) {
+        this.success = false;
+        this.msg = "";
+        this.obj = null;
+
         if (success != null) {
             this.success = success;
         }
@@ -22,24 +26,25 @@ class Msg {
 }
 
 class DBInbound {
-    id = 0;
-    userId = 0;
-    up = 0;
-    down = 0;
-    total = 0;
-    remark = "";
-    enable = true;
-    expiryTime = 0;
-
-    listen = "";
-    port = 0;
-    protocol = "";
-    settings = "";
-    streamSettings = "";
-    tag = "";
-    sniffing = "";
 
     constructor(data) {
+        this.id = 0;
+        this.userId = 0;
+        this.up = 0;
+        this.down = 0;
+        this.total = 0;
+        this.remark = "";
+        this.enable = true;
+        this.expiryTime = 0;
+
+        this.listen = "";
+        this.port = 0;
+        this.protocol = "";
+        this.settings = "";
+        this.streamSettings = "";
+        this.tag = "";
+        this.sniffing = "";
+
         if (data == null) {
             return;
         }
@@ -86,6 +91,25 @@ class DBInbound {
         return address;
     }
 
+    get _expiryTime() {
+        if (this.expiryTime === 0) {
+            return null;
+        }
+        return moment(this.expiryTime);
+    }
+
+    set _expiryTime(t) {
+        if (t == null) {
+            this.expiryTime = 0;
+        } else {
+            this.expiryTime = t.valueOf();
+        }
+    }
+
+    get isExpiry() {
+        return this.expiryTime < new Date().getTime();
+    }
+
     toInbound() {
         let settings = {};
         if (!ObjectUtil.isEmpty(this.settings)) {
@@ -132,17 +156,18 @@ class DBInbound {
 }
 
 class AllSetting {
-    webListen = "";
-    webPort = 54321;
-    webCertFile = "";
-    webKeyFile = "";
-    webBasePath = "/";
 
-    xrayTemplateConfig = "";
+    constructor(data) {
+        this.webListen = "";
+        this.webPort = 54321;
+        this.webCertFile = "";
+        this.webKeyFile = "";
+        this.webBasePath = "/";
 
-    timeLocation = "Asia/Shanghai";
+        this.xrayTemplateConfig = "";
+
+        this.timeLocation = "Asia/Shanghai";
 
-    constructor(data) {
         if (data == null) {
             return
         }

+ 1 - 1
web/assets/js/model/xray.js

@@ -1165,7 +1165,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
     }
 };
 Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
-    constructor(id=RandomUtil.randomUUID(), alterId=64) {
+    constructor(id=RandomUtil.randomUUID(), alterId=0) {
         super();
         this.id = id;
         this.alterId = alterId;

+ 15 - 14
web/assets/js/util/utils.js

@@ -77,18 +77,19 @@ class PromiseUtil {
 
 }
 
+const seq = [
+    'a', 'b', 'c', 'd', 'e', 'f', 'g',
+    'h', 'i', 'j', 'k', 'l', 'm', 'n',
+    'o', 'p', 'q', 'r', 's', 't',
+    'u', 'v', 'w', 'x', 'y', 'z',
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+    'A', 'B', 'C', 'D', 'E', 'F', 'G',
+    'H', 'I', 'J', 'K', 'L', 'M', 'N',
+    'O', 'P', 'Q', 'R', 'S', 'T',
+    'U', 'V', 'W', 'X', 'Y', 'Z'
+];
+
 class RandomUtil {
-    static seq = [
-        'a', 'b', 'c', 'd', 'e', 'f', 'g',
-        'h', 'i', 'j', 'k', 'l', 'm', 'n',
-        'o', 'p', 'q', 'r', 's', 't',
-        'u', 'v', 'w', 'x', 'y', 'z',
-        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
-        'A', 'B', 'C', 'D', 'E', 'F', 'G',
-        'H', 'I', 'J', 'K', 'L', 'M', 'N',
-        'O', 'P', 'Q', 'R', 'S', 'T',
-        'U', 'V', 'W', 'X', 'Y', 'Z'
-    ];
 
     static randomIntRange(min, max) {
         return parseInt(Math.random() * (max - min) + min, 10);
@@ -101,7 +102,7 @@ class RandomUtil {
     static randomSeq(count) {
         let str = '';
         for (let i = 0; i < count; ++i) {
-            str += this.seq[this.randomInt(62)];
+            str += seq[this.randomInt(62)];
         }
         return str;
     }
@@ -109,7 +110,7 @@ class RandomUtil {
     static randomLowerAndNum(count) {
         let str = '';
         for (let i = 0; i < count; ++i) {
-            str += this.seq[this.randomInt(36)];
+            str += seq[this.randomInt(36)];
         }
         return str;
     }
@@ -121,7 +122,7 @@ class RandomUtil {
             if (index <= 9) {
                 str += index;
             } else {
-                str += this.seq[index - 10];
+                str += seq[index - 10];
             }
         }
         return str;

+ 1 - 1
web/controller/inbound.go

@@ -37,7 +37,7 @@ func (a *InboundController) startTask() {
 	c := webServer.GetCron()
 	c.AddFunc("@every 10s", func() {
 		if a.xrayService.IsNeedRestartAndSetFalse() {
-			err := a.xrayService.RestartXray()
+			err := a.xrayService.RestartXray(false)
 			if err != nil {
 				logger.Error("restart xray failed:", err)
 			}

+ 13 - 0
web/html/xui/form/inbound.html

@@ -39,6 +39,19 @@
         </span>
         <a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
     </a-form-item>
+    <a-form-item>
+        <span slot="label">
+            到期时间
+            <a-tooltip>
+                <template slot="title">
+                    留空则永不到期
+                </template>
+                <a-icon type="question-circle" theme="filled"></a-icon>
+            </a-tooltip>
+        </span>
+        <a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
+                       v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
+    </a-form-item>
 </a-form>
 
 <!-- vmess settings -->

+ 68 - 24
web/html/xui/inbounds.html

@@ -46,24 +46,44 @@
                         <div slot="title">
                             <a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
                         </div>
-                        <a-input v-model="searchKey" placeholder="search" autofocus style="max-width: 300px"></a-input>
+<!--                        <a-input v-model="searchKey" placeholder="搜索" autofocus style="max-width: 300px"></a-input>-->
                         <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
                                  :data-source="dbInbounds"
                                  :loading="spinning" :scroll="{ x: 1500 }"
                                  :pagination="false"
                                  style="margin-top: 20px"
                                  @change="() => getDBInbounds()">
+                            <template slot="action" slot-scope="text, dbInbound">
+                                <a-dropdown :trigger="['click']">
+                                    <a @click="e => e.preventDefault()">操作</a>
+                                    <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
+                                        <a-menu-item v-if="dbInbound.hasLink()" key="qrcode">
+                                            <a-icon type="qrcode"></a-icon>二维码
+                                        </a-menu-item>
+                                        <a-menu-item key="edit">
+                                            <a-icon type="edit"></a-icon>编辑
+                                        </a-menu-item>
+                                        <a-menu-item key="resetTraffic">
+                                            <a-icon type="retweet"></a-icon>重置流量
+                                        </a-menu-item>
+                                        <a-menu-item key="delete">
+                                            <span style="color: #FF4D4F">
+                                                <a-icon type="delete"></a-icon>删除
+                                            </span>
+                                        </a-menu-item>
+                                    </a-menu>
+                                </a-dropdown>
+                            </template>
                             <template slot="protocol" slot-scope="text, dbInbound">
                                 <a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
                             </template>
                             <template slot="traffic" slot-scope="text, dbInbound">
-                                <a-tag color="blue">[[ sizeFormat(dbInbound.up) ]]</a-tag>
-                                <a-tag color="green">[[ sizeFormat(dbInbound.down) ]]</a-tag>
+                                <a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
                                 <template v-if="dbInbound.total > 0">
                                     <a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
                                     <a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag>
                                 </template>
-                                <a-tag v-else color="cyan">无限制</a-tag>
+                                <a-tag v-else color="green">无限制</a-tag>
                             </template>
                             <template slot="settings" slot-scope="text, dbInbound">
                                 <a-button type="link" @click="showInfo(dbInbound)">查看</a-button>
@@ -80,14 +100,15 @@
                                 <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
                             </template>
                             <template slot="expiryTime" slot-scope="text, dbInbound">
-                                <span v-if="dbInbound.expiryTime > 0" color="red">[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]</span>
-                                <span v-else>无限期</span>
-                            </template>
-                            <template slot="action" slot-scope="text, dbInbound">
-                                <a-button v-if="dbInbound.hasLink()" type="primary" icon="qrcode" @click="showQrcode(dbInbound)"></a-button>
-                                <a-button type="primary" icon="edit" @click="openEditInbound(dbInbound)"></a-button>
-                                <a-button icon="retweet" @click="resetTraffic(dbInbound)"></a-button>
-                                <a-button type="danger" icon="delete" @click="delInbound(dbInbound)"></a-button>
+                                <template v-if="dbInbound.expiryTime > 0">
+                                    <a-tag v-if="dbInbound.isExpiry" color="red">
+                                        [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
+                                    </a-tag>
+                                    <a-tag v-else color="blue">
+                                        [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
+                                    </a-tag>
+                                </template>
+                                <a-tag v-else color="green">无限期</a-tag>
                             </template>
                         </a-table>
                     </a-card>
@@ -100,14 +121,24 @@
 <script>
 
     const columns = [{
+        title: "操作",
+        align: 'center',
+        width: 30,
+        scopedSlots: { customRender: 'action' },
+    }, {
+        title: "启用",
+        align: 'center',
+        width: 40,
+        scopedSlots: { customRender: 'enable' },
+    }, {
         title: "id",
         align: 'center',
         dataIndex: "id",
-        width: 60,
+        width: 30,
     }, {
         title: "备注",
         align: 'center',
-        width: 60,
+        width: 100,
         dataIndex: "remark",
     }, {
         title: "协议",
@@ -122,12 +153,12 @@
     }, {
         title: "流量↑|↓",
         align: 'center',
-        width: 80,
+        width: 150,
         scopedSlots: { customRender: 'traffic' },
     }, {
         title: "详细信息",
         align: 'center',
-        width: 60,
+        width: 40,
         scopedSlots: { customRender: 'settings' },
     }, {
         title: "传输配置",
@@ -135,15 +166,10 @@
         width: 60,
         scopedSlots: { customRender: 'stream' },
     }, {
-        title: "启用",
-        align: 'center',
-        width: 60,
-        scopedSlots: { customRender: 'enable' },
-    }, {
-        title: "操作",
+        title: "到期时间",
         align: 'center',
-        width: 60,
-        scopedSlots: { customRender: 'action' },
+        width: 80,
+        scopedSlots: { customRender: 'expiryTime' },
     }];
 
     const app = new Vue({
@@ -190,6 +216,22 @@
                     });
                 }
             },
+            clickAction(action, dbInbound) {
+                switch (action.key) {
+                    case "qrcode":
+                        this.showQrcode(dbInbound);
+                        break;
+                    case "edit":
+                        this.openEditInbound(dbInbound);
+                        break;
+                    case "resetTraffic":
+                        this.resetTraffic(dbInbound);
+                        break;
+                    case "delete":
+                        this.delInbound(dbInbound);
+                        break;
+                }
+            },
             openAddInbound() {
                 inModal.show({
                     title: '添加入站',
@@ -222,6 +264,7 @@
                     total: dbInbound.total,
                     remark: dbInbound.remark,
                     enable: dbInbound.enable,
+                    expiryTime: dbInbound.expiryTime,
 
                     listen: inbound.listen,
                     port: inbound.port,
@@ -239,6 +282,7 @@
                     total: dbInbound.total,
                     remark: dbInbound.remark,
                     enable: dbInbound.enable,
+                    expiryTime: dbInbound.expiryTime,
 
                     listen: inbound.listen,
                     port: inbound.port,

+ 12 - 14
web/html/xui/index.html

@@ -188,8 +188,6 @@
     Object.freeze(State);
 
     class CurTotal {
-        current = 0
-        total = 0
 
         constructor(current, total) {
             this.current = current;
@@ -216,19 +214,19 @@
     }
 
     class Status {
-        cpu = new CurTotal(0, 0);
-        disk = new CurTotal(0, 0);
-        loads = [0, 0, 0];
-        mem = new CurTotal(0, 0);
-        netIO = {up: 0, down: 0};
-        netTraffic = {sent: 0, recv: 0};
-        swap = new CurTotal(0, 0);
-        tcpCount = 0;
-        udpCount = 0;
-        uptime = 0;
-        xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
-
         constructor(data) {
+            this.cpu = new CurTotal(0, 0);
+            this.disk = new CurTotal(0, 0);
+            this.loads = [0, 0, 0];
+            this.mem = new CurTotal(0, 0);
+            this.netIO = {up: 0, down: 0};
+            this.netTraffic = {sent: 0, recv: 0};
+            this.swap = new CurTotal(0, 0);
+            this.tcpCount = 0;
+            this.udpCount = 0;
+            this.uptime = 0;
+            this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
+
             if (data == null) {
                 return;
             }

+ 25 - 0
web/job/check_inbound_job.go

@@ -0,0 +1,25 @@
+package job
+
+import (
+	"x-ui/logger"
+	"x-ui/web/service"
+)
+
+type CheckInboundJob struct {
+	xrayService    service.XrayService
+	inboundService service.InboundService
+}
+
+func NewCheckInboundJob() *CheckInboundJob {
+	return new(CheckInboundJob)
+}
+
+func (j *CheckInboundJob) Run() {
+	count, err := j.inboundService.DisableInvalidInbounds()
+	if err != nil {
+		logger.Warning("disable invalid inbounds err:", err)
+	} else if count > 0 {
+		logger.Debugf("disabled %v inbounds", count)
+		j.xrayService.SetToNeedRestart()
+	}
+}

+ 25 - 0
web/job/check_xray_running_job.go

@@ -0,0 +1,25 @@
+package job
+
+import "x-ui/web/service"
+
+type CheckXrayRunningJob struct {
+	xrayService service.XrayService
+
+	checkTime int
+}
+
+func NewCheckXrayRunningJob() *CheckXrayRunningJob {
+	return new(CheckXrayRunningJob)
+}
+
+func (j *CheckXrayRunningJob) Run() {
+	if j.xrayService.IsXrayRunning() {
+		j.checkTime = 0
+		return
+	}
+	j.checkTime++
+	if j.checkTime < 2 {
+		return
+	}
+	j.xrayService.SetToNeedRestart()
+}

+ 30 - 0
web/job/xray_traffic_job.go

@@ -0,0 +1,30 @@
+package job
+
+import (
+	"x-ui/logger"
+	"x-ui/web/service"
+)
+
+type XrayTrafficJob struct {
+	xrayService    service.XrayService
+	inboundService service.InboundService
+}
+
+func NewXrayTrafficJob() *XrayTrafficJob {
+	return new(XrayTrafficJob)
+}
+
+func (j *XrayTrafficJob) Run() {
+	if !j.xrayService.IsXrayRunning() {
+		return
+	}
+	traffics, err := j.xrayService.GetXrayTraffic()
+	if err != nil {
+		logger.Warning("get xray traffic failed:", err)
+		return
+	}
+	err = j.inboundService.AddTraffic(traffics)
+	if err != nil {
+		logger.Warning("add traffic failed:", err)
+	}
+}

+ 21 - 0
web/network/auto_https_listener.go

@@ -0,0 +1,21 @@
+package network
+
+import "net"
+
+type AutoHttpsListener struct {
+	net.Listener
+}
+
+func NewAutoHttpsListener(listener net.Listener) net.Listener {
+	return &AutoHttpsListener{
+		Listener: listener,
+	}
+}
+
+func (l *AutoHttpsListener) Accept() (net.Conn, error) {
+	conn, err := l.Listener.Accept()
+	if err != nil {
+		return nil, err
+	}
+	return NewAutoHttpsConn(conn), nil
+}

+ 67 - 0
web/network/autp_https_conn.go

@@ -0,0 +1,67 @@
+package network
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"net"
+	"net/http"
+	"sync"
+)
+
+type AutoHttpsConn struct {
+	net.Conn
+
+	firstBuf []byte
+	bufStart int
+
+	readRequestOnce sync.Once
+}
+
+func NewAutoHttpsConn(conn net.Conn) net.Conn {
+	return &AutoHttpsConn{
+		Conn: conn,
+	}
+}
+
+func (c *AutoHttpsConn) readRequest() bool {
+	c.firstBuf = make([]byte, 2048)
+	n, err := c.Conn.Read(c.firstBuf)
+	c.firstBuf = c.firstBuf[:n]
+	if err != nil {
+		return false
+	}
+	reader := bytes.NewReader(c.firstBuf)
+	bufReader := bufio.NewReader(reader)
+	request, err := http.ReadRequest(bufReader)
+	if err != nil {
+		return false
+	}
+	resp := http.Response{
+		Header: http.Header{},
+	}
+	resp.StatusCode = http.StatusTemporaryRedirect
+	location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
+	resp.Header.Set("Location", location)
+	resp.Write(c.Conn)
+	c.Close()
+	c.firstBuf = nil
+	return true
+}
+
+func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
+	c.readRequestOnce.Do(func() {
+		c.readRequest()
+	})
+
+	if c.firstBuf != nil {
+		n := copy(buf, c.firstBuf[c.bufStart:])
+		c.bufStart += n
+		if c.bufStart >= len(c.firstBuf) {
+			c.firstBuf = nil
+		}
+		return n, nil
+	}
+
+	return c.Conn.Read(buf)
+}

+ 3 - 1
web/service/inbound.go

@@ -3,6 +3,7 @@ package service
 import (
 	"fmt"
 	"gorm.io/gorm"
+	"time"
 	"x-ui/database"
 	"x-ui/database/model"
 	"x-ui/util/common"
@@ -166,8 +167,9 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
 
 func (s *InboundService) DisableInvalidInbounds() (int64, error) {
 	db := database.GetDB()
+	now := time.Now()
 	result := db.Model(model.Inbound{}).
-		Where("up + down >= total and total > 0 and enable = ?", true).
+		Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
 		Update("enable", false)
 	err := result.Error
 	count := result.RowsAffected

+ 6 - 9
web/service/server.go

@@ -18,6 +18,7 @@ import (
 	"runtime"
 	"time"
 	"x-ui/logger"
+	"x-ui/util/sys"
 	"x-ui/xray"
 )
 
@@ -142,18 +143,14 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
 		logger.Warning("can not find io counters")
 	}
 
-	tcpConnStats, err := net.Connections("tcp")
+	status.TcpCount, err = sys.GetTCPCount()
 	if err != nil {
-		logger.Warning("get connections failed:", err)
-	} else {
-		status.TcpCount = len(tcpConnStats)
+		logger.Warning("get tcp connections failed:", err)
 	}
 
-	udpConnStats, err := net.Connections("udp")
+	status.UdpCount, err = sys.GetUDPCount()
 	if err != nil {
-		logger.Warning("get connections failed:", err)
-	} else {
-		status.UdpCount = len(udpConnStats)
+		logger.Warning("get udp connections failed:", err)
 	}
 
 	if s.xrayService.IsXrayRunning() {
@@ -265,7 +262,7 @@ func (s *ServerService) UpdateXray(version string) error {
 
 	s.xrayService.StopXray()
 	defer func() {
-		err := s.xrayService.RestartXray()
+		err := s.xrayService.RestartXray(true)
 		if err != nil {
 			logger.Error("start xray failed:", err)
 		}

+ 4 - 3
web/service/xray.go

@@ -84,7 +84,7 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) {
 	return p.GetTraffic(true)
 }
 
-func (s *XrayService) RestartXray() error {
+func (s *XrayService) RestartXray(isForce bool) error {
 	lock.Lock()
 	defer lock.Unlock()
 	logger.Debug("restart xray")
@@ -94,8 +94,9 @@ func (s *XrayService) RestartXray() error {
 		return err
 	}
 
-	if p != nil {
-		if p.GetConfig().Equals(xrayConfig) {
+	if p != nil && p.IsRunning() {
+		if !isForce && p.GetConfig().Equals(xrayConfig) {
+			logger.Debug("not need to restart xray")
 			return nil
 		}
 		p.Stop()

+ 18 - 47
web/web.go

@@ -24,6 +24,8 @@ import (
 	"x-ui/logger"
 	"x-ui/util/common"
 	"x-ui/web/controller"
+	"x-ui/web/job"
+	"x-ui/web/network"
 	"x-ui/web/service"
 )
 
@@ -277,53 +279,21 @@ func (s *Server) initI18n(engine *gin.Engine) error {
 }
 
 func (s *Server) startTask() {
-	err := s.xrayService.RestartXray()
+	err := s.xrayService.RestartXray(true)
 	if err != nil {
 		logger.Warning("start xray failed:", err)
 	}
-	var checkTime = 0
 	// 每 30 秒检查一次 xray 是否在运行
-	s.cron.AddFunc("@every 30s", func() {
-		if s.xrayService.IsXrayRunning() {
-			checkTime = 0
-			return
-		}
-		checkTime++
-		if checkTime < 2 {
-			return
-		}
-		s.xrayService.SetToNeedRestart()
-	})
+	s.cron.AddJob("@every 30s", job.NewCheckXrayRunningJob())
 
 	go func() {
 		time.Sleep(time.Second * 5)
 		// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
-		s.cron.AddFunc("@every 10s", func() {
-			if !s.xrayService.IsXrayRunning() {
-				return
-			}
-			traffics, err := s.xrayService.GetXrayTraffic()
-			if err != nil {
-				logger.Warning("get xray traffic failed:", err)
-				return
-			}
-			err = s.inboundService.AddTraffic(traffics)
-			if err != nil {
-				logger.Warning("add traffic failed:", err)
-			}
-		})
+		s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
 	}()
 
-	// 每 30 秒检查一次 inbound 流量超出情况
-	s.cron.AddFunc("@every 30s", func() {
-		count, err := s.inboundService.DisableInvalidInbounds()
-		if err != nil {
-			logger.Warning("disable invalid inbounds err:", err)
-		} else if count > 0 {
-			logger.Debugf("disabled %v inbounds", count)
-			s.xrayService.SetToNeedRestart()
-		}
-	})
+	// 每 30 秒检查一次 inbound 流量超出和到期的情况
+	s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
 }
 
 func (s *Server) Start() (err error) {
@@ -362,22 +332,21 @@ func (s *Server) Start() (err error) {
 		return err
 	}
 	listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
-	var listener net.Listener
+	listener, err := net.Listen("tcp", listenAddr)
+	if err != nil {
+		return err
+	}
 	if certFile != "" || keyFile != "" {
-		var cert tls.Certificate
-		cert, err = tls.LoadX509KeyPair(certFile, keyFile)
+		cert, err := tls.LoadX509KeyPair(certFile, keyFile)
 		if err != nil {
+			listener.Close()
 			return err
 		}
 		c := &tls.Config{
 			Certificates: []tls.Certificate{cert},
 		}
-		listener, err = tls.Listen("tcp", listenAddr, c)
-	} else {
-		listener, err = net.Listen("tcp", listenAddr)
-	}
-	if err != nil {
-		return err
+		listener = network.NewAutoHttpsListener(listener)
+		listener = tls.NewListener(listener, c)
 	}
 	if certFile != "" || keyFile != "" {
 		logger.Info("web server run https on", listener.Addr())
@@ -392,7 +361,9 @@ func (s *Server) Start() (err error) {
 		Handler: engine,
 	}
 
-	go s.httpServer.Serve(listener)
+	go func() {
+		s.httpServer.Serve(listener)
+	}()
 
 	return nil
 }