Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0d88e9ad7 | |||
|
|
ba9edd3c02 |
@@ -55,9 +55,21 @@ type Client struct {
|
|||||||
|
|
||||||
// NewClient 创建客户端
|
// NewClient 创建客户端
|
||||||
func NewClient(serverAddr, token, id string) *Client {
|
func NewClient(serverAddr, token, id string) *Client {
|
||||||
// 默认数据目录
|
// 默认数据目录:优先使用用户主目录,失败时回退到当前工作目录
|
||||||
home, _ := os.UserHomeDir()
|
var dataDir string
|
||||||
dataDir := filepath.Join(home, ".gotunnel")
|
if home, err := os.UserHomeDir(); err == nil && home != "" {
|
||||||
|
dataDir = filepath.Join(home, ".gotunnel")
|
||||||
|
} else {
|
||||||
|
// UserHomeDir 失败(如 Android adb shell 环境),使用当前工作目录
|
||||||
|
if cwd, err := os.Getwd(); err == nil {
|
||||||
|
dataDir = filepath.Join(cwd, ".gotunnel")
|
||||||
|
log.Printf("[Client] UserHomeDir unavailable, using current directory: %s", dataDir)
|
||||||
|
} else {
|
||||||
|
// 最后回退到相对路径
|
||||||
|
dataDir = ".gotunnel"
|
||||||
|
log.Printf("[Client] Warning: using relative path for data directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 确保数据目录存在
|
// 确保数据目录存在
|
||||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||||
@@ -889,58 +901,99 @@ func (c *Client) sendUpdateResult(stream net.Conn, success bool, message string)
|
|||||||
func (c *Client) performSelfUpdate(downloadURL string) error {
|
func (c *Client) performSelfUpdate(downloadURL string) error {
|
||||||
c.logf("Starting self-update from: %s", downloadURL)
|
c.logf("Starting self-update from: %s", downloadURL)
|
||||||
|
|
||||||
// 使用共享的下载和解压逻辑
|
|
||||||
binaryPath, cleanup, err := update.DownloadAndExtract(downloadURL, "client")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
// 获取当前可执行文件路径
|
// 获取当前可执行文件路径
|
||||||
currentPath, err := os.Executable()
|
currentPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get executable: %w", err)
|
c.logErrorf("Update failed: cannot get executable path: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
currentPath, _ = filepath.EvalSymlinks(currentPath)
|
currentPath, _ = filepath.EvalSymlinks(currentPath)
|
||||||
|
|
||||||
|
// 预检查:验证是否有写权限(在下载前检查,避免浪费带宽)
|
||||||
|
if err := c.checkUpdatePermissions(currentPath); err != nil {
|
||||||
|
c.logErrorf("Update failed: %v", err)
|
||||||
|
c.logErrorf("Self-update is not supported in this environment. Please update manually.")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用共享的下载和解压逻辑
|
||||||
|
c.logf("Downloading update package...")
|
||||||
|
binaryPath, cleanup, err := update.DownloadAndExtract(downloadURL, "client")
|
||||||
|
if err != nil {
|
||||||
|
c.logErrorf("Update failed: download/extract error: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
// Windows 需要特殊处理
|
// Windows 需要特殊处理
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return performWindowsClientUpdate(binaryPath, currentPath, c.ServerAddr, c.Token, c.ID)
|
return performWindowsClientUpdate(binaryPath, currentPath, c.ServerAddr, c.Token, c.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux/Mac: 直接替换
|
// Linux/Mac/Android: 直接替换
|
||||||
backupPath := currentPath + ".bak"
|
backupPath := currentPath + ".bak"
|
||||||
|
|
||||||
// 停止所有插件
|
// 停止所有插件
|
||||||
c.stopAllPlugins()
|
c.stopAllPlugins()
|
||||||
|
|
||||||
// 备份当前文件
|
// 备份当前文件
|
||||||
|
c.logf("Backing up current binary...")
|
||||||
if err := os.Rename(currentPath, backupPath); err != nil {
|
if err := os.Rename(currentPath, backupPath); err != nil {
|
||||||
return fmt.Errorf("backup current: %w", err)
|
c.logErrorf("Update failed: cannot backup current binary: %v", err)
|
||||||
|
c.logErrorf("This may be due to insufficient permissions or read-only filesystem.")
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制新文件(不能用 rename,可能跨文件系统)
|
// 复制新文件(不能用 rename,可能跨文件系统)
|
||||||
|
c.logf("Installing new binary...")
|
||||||
if err := update.CopyFile(binaryPath, currentPath); err != nil {
|
if err := update.CopyFile(binaryPath, currentPath); err != nil {
|
||||||
os.Rename(backupPath, currentPath)
|
os.Rename(backupPath, currentPath)
|
||||||
return fmt.Errorf("replace binary: %w", err)
|
c.logErrorf("Update failed: cannot install new binary: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置执行权限
|
// 设置执行权限
|
||||||
if err := os.Chmod(currentPath, 0755); err != nil {
|
if err := os.Chmod(currentPath, 0755); err != nil {
|
||||||
os.Rename(backupPath, currentPath)
|
os.Rename(backupPath, currentPath)
|
||||||
return fmt.Errorf("chmod: %w", err)
|
c.logErrorf("Update failed: cannot set execute permission: %v", err)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除备份
|
// 删除备份
|
||||||
os.Remove(backupPath)
|
os.Remove(backupPath)
|
||||||
|
|
||||||
c.logf("Update completed, restarting...")
|
c.logf("Update completed successfully, restarting...")
|
||||||
|
|
||||||
// 重启进程
|
// 重启进程
|
||||||
restartClientProcess(currentPath, c.ServerAddr, c.Token, c.ID)
|
restartClientProcess(currentPath, c.ServerAddr, c.Token, c.ID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkUpdatePermissions 检查是否有更新权限
|
||||||
|
func (c *Client) checkUpdatePermissions(execPath string) error {
|
||||||
|
// 检查可执行文件所在目录是否可写
|
||||||
|
dir := filepath.Dir(execPath)
|
||||||
|
testFile := filepath.Join(dir, ".gotunnel_update_test")
|
||||||
|
|
||||||
|
f, err := os.Create(testFile)
|
||||||
|
if err != nil {
|
||||||
|
c.logErrorf("No write permission to directory: %s", dir)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
os.Remove(testFile)
|
||||||
|
|
||||||
|
// 检查可执行文件本身是否可写
|
||||||
|
f, err = os.OpenFile(execPath, os.O_WRONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
c.logErrorf("No write permission to executable: %s", execPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// stopAllPlugins 停止所有运行中的插件
|
// stopAllPlugins 停止所有运行中的插件
|
||||||
func (c *Client) stopAllPlugins() {
|
func (c *Client) stopAllPlugins() {
|
||||||
c.pluginMu.Lock()
|
c.pluginMu.Lock()
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ func generateClientID() string {
|
|||||||
// Server 隧道服务端
|
// Server 隧道服务端
|
||||||
type Server struct {
|
type Server struct {
|
||||||
clientStore db.ClientStore
|
clientStore db.ClientStore
|
||||||
jsPluginStore db.JSPluginStore // JS 插件存储
|
jsPluginStore db.JSPluginStore // JS 插件存储
|
||||||
trafficStore db.TrafficStore // 流量存储
|
trafficStore db.TrafficStore // 流量存储
|
||||||
bindAddr string
|
bindAddr string
|
||||||
bindPort int
|
bindPort int
|
||||||
token string
|
token string
|
||||||
@@ -514,8 +514,8 @@ func (s *Server) acceptProxyConns(cs *ClientSession, ln net.Listener, rule proto
|
|||||||
func (s *Server) acceptProxyServerConns(cs *ClientSession, ln net.Listener, rule protocol.ProxyRule) {
|
func (s *Server) acceptProxyServerConns(cs *ClientSession, ln net.Listener, rule protocol.ProxyRule) {
|
||||||
dialer := proxy.NewTunnelDialer(cs.Session)
|
dialer := proxy.NewTunnelDialer(cs.Session)
|
||||||
|
|
||||||
// 使用内置 proxy 实现
|
// 使用内置 proxy 实现 (带流量统计)
|
||||||
proxyServer := proxy.NewServer(rule.Type, dialer)
|
proxyServer := proxy.NewServer(rule.Type, dialer, s.recordTraffic)
|
||||||
for {
|
for {
|
||||||
conn, err := ln.Accept()
|
conn, err := ln.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -963,6 +963,9 @@ func (s *Server) sendUDPPacket(cs *ClientSession, conn *net.UDPConn, clientAddr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录入站流量 (从外部接收的数据)
|
||||||
|
s.recordTraffic(int64(len(packet.Data)), 0)
|
||||||
|
|
||||||
// 等待客户端响应
|
// 等待客户端响应
|
||||||
respMsg, err := protocol.ReadMessage(stream)
|
respMsg, err := protocol.ReadMessage(stream)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -975,6 +978,8 @@ func (s *Server) sendUDPPacket(cs *ClientSession, conn *net.UDPConn, clientAddr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
conn.WriteToUDP(respPacket.Data, clientAddr)
|
conn.WriteToUDP(respPacket.Data, clientAddr)
|
||||||
|
// 记录出站流量 (发送回外部的数据)
|
||||||
|
s.recordTraffic(0, int64(len(respPacket.Data)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,19 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gotunnel/pkg/relay"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPServer HTTP 代理服务
|
// HTTPServer HTTP 代理服务
|
||||||
type HTTPServer struct {
|
type HTTPServer struct {
|
||||||
dialer Dialer
|
dialer Dialer
|
||||||
|
onStats func(in, out int64) // 流量统计回调
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHTTPServer 创建 HTTP 代理服务
|
// NewHTTPServer 创建 HTTP 代理服务
|
||||||
func NewHTTPServer(dialer Dialer) *HTTPServer {
|
func NewHTTPServer(dialer Dialer, onStats func(in, out int64)) *HTTPServer {
|
||||||
return &HTTPServer{dialer: dialer}
|
return &HTTPServer{dialer: dialer, onStats: onStats}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleConn 处理 HTTP 代理连接
|
// HandleConn 处理 HTTP 代理连接
|
||||||
@@ -50,8 +53,8 @@ func (h *HTTPServer) handleConnect(conn net.Conn, req *http.Request) error {
|
|||||||
|
|
||||||
conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
|
conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
|
||||||
|
|
||||||
go io.Copy(remote, conn)
|
// 双向转发 (带流量统计)
|
||||||
io.Copy(conn, remote)
|
relay.RelayWithStats(conn, remote, h.onStats)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +85,10 @@ func (h *HTTPServer) handleHTTP(conn net.Conn, req *http.Request, reader *bufio.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转发响应
|
// 转发响应 (带流量统计)
|
||||||
_, err = io.Copy(conn, remote)
|
n, err := io.Copy(conn, remote)
|
||||||
|
if h.onStats != nil && n > 0 {
|
||||||
|
h.onStats(0, n) // 响应数据为出站流量
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ type Server struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewServer 创建代理服务器
|
// NewServer 创建代理服务器
|
||||||
func NewServer(typ string, dialer Dialer) *Server {
|
func NewServer(typ string, dialer Dialer, onStats func(in, out int64)) *Server {
|
||||||
return &Server{
|
return &Server{
|
||||||
socks5: NewSOCKS5Server(dialer),
|
socks5: NewSOCKS5Server(dialer, onStats),
|
||||||
http: NewHTTPServer(dialer),
|
http: NewHTTPServer(dialer, onStats),
|
||||||
typ: typ,
|
typ: typ,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/gotunnel/pkg/relay"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -19,7 +21,8 @@ const (
|
|||||||
|
|
||||||
// SOCKS5Server SOCKS5 代理服务
|
// SOCKS5Server SOCKS5 代理服务
|
||||||
type SOCKS5Server struct {
|
type SOCKS5Server struct {
|
||||||
dialer Dialer
|
dialer Dialer
|
||||||
|
onStats func(in, out int64) // 流量统计回调
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dialer 连接拨号器接口
|
// Dialer 连接拨号器接口
|
||||||
@@ -28,8 +31,8 @@ type Dialer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewSOCKS5Server 创建 SOCKS5 服务
|
// NewSOCKS5Server 创建 SOCKS5 服务
|
||||||
func NewSOCKS5Server(dialer Dialer) *SOCKS5Server {
|
func NewSOCKS5Server(dialer Dialer, onStats func(in, out int64)) *SOCKS5Server {
|
||||||
return &SOCKS5Server{dialer: dialer}
|
return &SOCKS5Server{dialer: dialer, onStats: onStats}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleConn 处理 SOCKS5 连接
|
// HandleConn 处理 SOCKS5 连接
|
||||||
@@ -60,9 +63,8 @@ func (s *SOCKS5Server) HandleConn(conn net.Conn) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 双向转发
|
// 双向转发 (带流量统计)
|
||||||
go io.Copy(remote, conn)
|
relay.RelayWithStats(conn, remote, s.onStats)
|
||||||
io.Copy(conn, remote)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ref, onMounted, onUnmounted } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
ArrowBackOutline, CreateOutline, TrashOutline,
|
ArrowBackOutline, CreateOutline, TrashOutline,
|
||||||
PushOutline, AddOutline, StorefrontOutline, DocumentTextOutline,
|
PushOutline, AddOutline, StorefrontOutline,
|
||||||
ExtensionPuzzleOutline, SettingsOutline, RefreshOutline
|
ExtensionPuzzleOutline, SettingsOutline, RefreshOutline
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import GlassModal from '../components/GlassModal.vue'
|
import GlassModal from '../components/GlassModal.vue'
|
||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
type UpdateInfo, type SystemStats
|
type UpdateInfo, type SystemStats
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import type { ProxyRule, ClientPlugin, ConfigField, StorePluginInfo, RuleSchemasMap } from '../types'
|
import type { ProxyRule, ClientPlugin, ConfigField, StorePluginInfo, RuleSchemasMap } from '../types'
|
||||||
import LogViewer from '../components/LogViewer.vue'
|
|
||||||
import InlineLogPanel from '../components/InlineLogPanel.vue'
|
import InlineLogPanel from '../components/InlineLogPanel.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -513,9 +512,6 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Log Viewer
|
|
||||||
const showLogViewer = ref(false)
|
|
||||||
|
|
||||||
// Plugin Menu
|
// Plugin Menu
|
||||||
const activePluginMenu = ref('')
|
const activePluginMenu = ref('')
|
||||||
const togglePluginMenu = (pluginId: string) => {
|
const togglePluginMenu = (pluginId: string) => {
|
||||||
@@ -578,10 +574,6 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
|
|||||||
<PushOutline class="btn-icon" />
|
<PushOutline class="btn-icon" />
|
||||||
<span>推送配置</span>
|
<span>推送配置</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="glass-btn" @click="showLogViewer=true">
|
|
||||||
<DocumentTextOutline class="btn-icon" />
|
|
||||||
<span>日志</span>
|
|
||||||
</button>
|
|
||||||
<button class="glass-btn danger" @click="confirmDelete">
|
<button class="glass-btn danger" @click="confirmDelete">
|
||||||
<TrashOutline class="btn-icon" />
|
<TrashOutline class="btn-icon" />
|
||||||
<span>删除</span>
|
<span>删除</span>
|
||||||
@@ -905,8 +897,6 @@ const handleDeletePlugin = (plugin: ClientPlugin) => {
|
|||||||
<button class="glass-btn primary" @click="confirmInstallPlugin">确认安装</button>
|
<button class="glass-btn primary" @click="confirmInstallPlugin">确认安装</button>
|
||||||
</template>
|
</template>
|
||||||
</GlassModal>
|
</GlassModal>
|
||||||
|
|
||||||
<LogViewer :visible="showLogViewer" @close="showLogViewer = false" :client-id="clientId" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user