feat: 重构了展示页面,并且提供了连接的监控

This commit is contained in:
qcqcqc@wsl 2026-01-09 14:30:09 +08:00
parent 0f47e943f4
commit cdc1972ffa
10 changed files with 2008 additions and 874 deletions

View File

@ -10,4 +10,7 @@ require (
gopkg.in/yaml.v3 v3.0.1
)
require golang.org/x/sys v0.37.0
require (
github.com/google/uuid v1.6.0
golang.org/x/sys v0.37.0
)

View File

@ -1,3 +1,5 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=

View File

@ -61,13 +61,19 @@ type Response struct {
// RegisterRoutes 注册路由
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
// API 路由
mux.HandleFunc("/api/mapping/create", h.authMiddleware(h.handleCreateMapping))
mux.HandleFunc("/api/mapping/remove", h.authMiddleware(h.handleRemoveMapping))
mux.HandleFunc("/api/mapping/list", h.authMiddleware(h.handleListMappings))
mux.HandleFunc("/api/stats/traffic", h.authMiddleware(h.handleGetTrafficStats))
mux.HandleFunc("/api/stats/history", h.authMiddleware(h.handleGetTrafficHistory))
mux.HandleFunc("/api/stats/monitor", h.authMiddleware(h.handleTrafficMonitor))
mux.HandleFunc("/admin", h.handleManagement)
mux.HandleFunc("/api/stats/connections", h.authMiddleware(h.handleGetActiveConnections))
// 页面路由
mux.HandleFunc("/", h.handleRoot)
mux.HandleFunc("/login", h.handleLogin)
mux.HandleFunc("/dashboard", h.handleDashboard)
mux.HandleFunc("/health", h.handleHealth)
}
@ -384,10 +390,31 @@ func (h *Handler) handleTrafficMonitor(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, GetTraffticMonitorHTML())
}
// handleManagement 管理页面
func (h *Handler) handleManagement(w http.ResponseWriter, r *http.Request) {
// handleConnections 连接监控页面
func (h *Handler) handleConnections(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, GetManagementHTML())
fmt.Fprint(w, GetConnectionsHTML())
}
// handleRoot 根路径重定向
func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
http.Redirect(w, r, "/login", http.StatusFound)
}
// handleLogin 登录页面
func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, GetLoginHTML())
}
// handleDashboard 仪表板页面
func (h *Handler) handleDashboard(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, GetDashboardHTML())
}
// handleGetTrafficHistory 获取历史流量统计
@ -436,3 +463,27 @@ func (h *Handler) handleGetTrafficHistory(w http.ResponseWriter, r *http.Request
"count": len(records),
})
}
// handleGetActiveConnections 获取所有活跃连接信息
func (h *Handler) handleGetActiveConnections(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
h.writeError(w, http.StatusMethodNotAllowed, "只支持 GET 方法")
return
}
// 获取所有活跃连接
connectionsStats := h.forwarderMgr.GetAllActiveConnections()
// 按端口号排序
sort.Slice(connectionsStats, func(i, j int) bool {
return connectionsStats[i].SourcePort < connectionsStats[j].SourcePort
})
// 构建响应
response := stats.AllConnectionsStats{
Mappings: connectionsStats,
Timestamp: time.Now().Unix(),
}
h.writeSuccess(w, "获取活跃连接成功", response)
}

View File

@ -29,3 +29,45 @@ func GetManagementHTML() string {
}
return string(buf[:n])
}
func GetConnectionsHTML() string {
file, err := html.AssetsFS.Open("connections.html")
if err != nil {
return "<h1>Failed to load HTML</h1>"
}
defer file.Close()
buf := make([]byte, 40960) // 40KB buffer
n, err := file.Read(buf)
if err != nil {
return "<h1>Failed to read HTML</h1>"
}
return string(buf[:n])
}
func GetLoginHTML() string {
file, err := html.AssetsFS.Open("login.html")
if err != nil {
return "<h1>Failed to load HTML</h1>"
}
defer file.Close()
buf := make([]byte, 40960) // 40KB buffer
n, err := file.Read(buf)
if err != nil {
return "<h1>Failed to read HTML</h1>"
}
return string(buf[:n])
}
func GetDashboardHTML() string {
file, err := html.AssetsFS.Open("dashboard.html")
if err != nil {
return "<h1>Failed to load HTML</h1>"
}
defer file.Close()
buf := make([]byte, 81920) // 80KB buffer for dashboard
n, err := file.Read(buf)
if err != nil {
return "<h1>Failed to read HTML</h1>"
}
return string(buf[:n])
}

View File

@ -12,6 +12,8 @@ import (
"time"
"golang.org/x/time/rate"
"github.com/google/uuid"
)
// TunnelServer 隧道服务器接口
@ -39,6 +41,19 @@ type Forwarder struct {
bytesReceived uint64 // 接收字节数
limiterOut *rate.Limiter // 限速器(出方向)
limiterIn *rate.Limiter // 限速器(入方向)
// 活跃连接管理
connections map[string]*activeConnection // 活跃连接映射
connMutex sync.RWMutex // 连接映射锁
}
// activeConnection 活跃连接信息
type activeConnection struct {
clientAddr string
targetAddr string
bytesSent uint64
bytesReceived uint64
connectedAt int64
}
// NewForwarder 创建新的端口转发器
@ -57,15 +72,16 @@ func NewForwarder(sourcePort int, targetHost string, targetPort int, limit *int6
limiterIn = rate.NewLimiter(rate.Limit(*limit), burst)
}
return &Forwarder{
sourcePort: sourcePort,
targetPort: targetPort,
targetHost: targetHost,
cancel: cancel,
ctx: ctx,
useTunnel: false,
limit: limit,
limiterOut: limiterOut,
limiterIn: limiterIn,
sourcePort: sourcePort,
targetPort: targetPort,
targetHost: targetHost,
cancel: cancel,
ctx: ctx,
useTunnel: false,
limit: limit,
limiterOut: limiterOut,
limiterIn: limiterIn,
connections: make(map[string]*activeConnection),
}
}
@ -95,6 +111,7 @@ func NewTunnelForwarder(sourcePort int, targetHost string, targetPort int, tunne
limit: limit,
limiterOut: limiterOut,
limiterIn: limiterIn,
connections: make(map[string]*activeConnection),
}
}
@ -189,15 +206,19 @@ func (rlr *rateLimitedReader) Read(p []byte) (int, error) {
// countingWriter 带统计的 Writer
type countingWriter struct {
w io.Writer
counter *uint64
port int
w io.Writer
counter *uint64
port int
connCounter *uint64 // 连接级别的计数器
}
func (cw *countingWriter) Write(p []byte) (int, error) {
n, err := cw.w.Write(p)
if n > 0 {
atomic.AddUint64(cw.counter, uint64(n))
if cw.connCounter != nil {
atomic.AddUint64(cw.connCounter, uint64(n))
}
}
return n, err
}
@ -207,7 +228,11 @@ func (f *Forwarder) handleConnection(clientConn net.Conn) {
defer f.wg.Done()
defer clientConn.Close()
log.Printf("端口 %d 收到新连接: %s", f.sourcePort, clientConn.RemoteAddr())
// 生成连接ID
connID := uuid.New().String()
clientAddr := clientConn.RemoteAddr().String()
log.Printf("端口 %d 收到新连接: %s (连接ID: %s)", f.sourcePort, clientAddr, connID)
var targetConn net.Conn
var err error
@ -243,6 +268,30 @@ func (f *Forwarder) handleConnection(clientConn net.Conn) {
defer targetConn.Close()
// 记录活跃连接
targetAddr := fmt.Sprintf("%s:%d", f.targetHost, f.targetPort)
conn := &activeConnection{
clientAddr: clientAddr,
targetAddr: targetAddr,
bytesSent: 0,
bytesReceived: 0,
connectedAt: time.Now().Unix(),
}
f.connMutex.Lock()
f.connections[connID] = conn
f.connMutex.Unlock()
// 连接关闭时移除记录
defer func() {
f.connMutex.Lock()
delete(f.connections, connID)
f.connMutex.Unlock()
log.Printf("端口 %d 连接关闭: %s (连接ID: %s, 发送: %d, 接收: %d)",
f.sourcePort, clientAddr, connID,
atomic.LoadUint64(&conn.bytesSent),
atomic.LoadUint64(&conn.bytesReceived))
}()
// 双向转发
var wg sync.WaitGroup
wg.Add(2)
@ -256,9 +305,10 @@ func (f *Forwarder) handleConnection(clientConn net.Conn) {
ctx: f.ctx,
}
writer := &countingWriter{
w: targetConn,
counter: &f.bytesSent,
port: f.sourcePort,
w: targetConn,
counter: &f.bytesSent,
port: f.sourcePort,
connCounter: &conn.bytesSent,
}
n, _ := io.Copy(writer, reader)
log.Printf("端口 %d: 客户端->目标传输完成,本次发送 %d 字节 (总发送: %d)", f.sourcePort, n, atomic.LoadUint64(&f.bytesSent))
@ -277,9 +327,10 @@ func (f *Forwarder) handleConnection(clientConn net.Conn) {
ctx: f.ctx,
}
writer := &countingWriter{
w: clientConn,
counter: &f.bytesReceived,
port: f.sourcePort,
w: clientConn,
counter: &f.bytesReceived,
port: f.sourcePort,
connCounter: &conn.bytesReceived,
}
n, _ := io.Copy(writer, reader)
log.Printf("端口 %d: 目标->客户端传输完成,本次接收 %d 字节 (总接收: %d)", f.sourcePort, n, atomic.LoadUint64(&f.bytesReceived))
@ -442,3 +493,43 @@ func (m *Manager) GetAllTrafficStats() map[int]stats.TrafficStats {
return statsMap
}
// GetActiveConnections 获取某个转发器的活跃连接信息
func (f *Forwarder) GetActiveConnections() []stats.ConnectionInfo {
f.connMutex.RLock()
defer f.connMutex.RUnlock()
connections := make([]stats.ConnectionInfo, 0, len(f.connections))
for _, conn := range f.connections {
connections = append(connections, stats.ConnectionInfo{
ClientAddr: conn.clientAddr,
TargetAddr: conn.targetAddr,
BytesSent: atomic.LoadUint64(&conn.bytesSent),
BytesReceived: atomic.LoadUint64(&conn.bytesReceived),
ConnectedAt: conn.connectedAt,
})
}
return connections
}
// GetAllActiveConnections 获取所有转发器的活跃连接信息
func (m *Manager) GetAllActiveConnections() []stats.PortConnectionStats {
m.mu.RLock()
defer m.mu.RUnlock()
allStats := make([]stats.PortConnectionStats, 0, len(m.forwarders))
for port, forwarder := range m.forwarders {
connections := forwarder.GetActiveConnections()
allStats = append(allStats, stats.PortConnectionStats{
SourcePort: port,
TargetHost: forwarder.targetHost,
TargetPort: forwarder.targetPort,
UseTunnel: forwarder.useTunnel,
ActiveConnections: connections,
TotalConnections: len(connections),
})
}
return allStats
}

File diff suppressed because it is too large Load Diff

428
src/server/html/login.html Normal file
View File

@ -0,0 +1,428 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - Go Tunnel 管理系统</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
width: 100%;
max-width: 900px;
display: flex;
min-height: 500px;
}
.login-left {
flex: 1;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 60px 40px;
display: flex;
flex-direction: column;
justify-content: center;
color: white;
}
.login-left h1 {
font-size: 2.5em;
margin-bottom: 20px;
font-weight: 700;
}
.login-left p {
font-size: 1.1em;
line-height: 1.6;
opacity: 0.95;
margin-bottom: 30px;
}
.feature-list {
list-style: none;
margin-top: 20px;
}
.feature-list li {
padding: 10px 0;
display: flex;
align-items: center;
font-size: 1em;
}
.feature-list li:before {
content: "✓";
margin-right: 10px;
font-weight: bold;
font-size: 1.2em;
}
.login-right {
flex: 1;
padding: 60px 40px;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-header {
text-align: center;
margin-bottom: 40px;
}
.login-header h2 {
color: #333;
font-size: 2em;
margin-bottom: 10px;
}
.login-header p {
color: #666;
font-size: 1em;
}
.form-group {
margin-bottom: 25px;
}
.form-group label {
display: block;
color: #333;
font-weight: 600;
margin-bottom: 8px;
font-size: 0.95em;
}
.form-group input {
width: 100%;
padding: 14px 16px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 1em;
transition: all 0.3s;
background: #f8f9fa;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
background: white;
box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
}
.remember-me {
display: flex;
align-items: center;
margin-bottom: 25px;
}
.remember-me input[type="checkbox"] {
width: 18px;
height: 18px;
margin-right: 8px;
cursor: pointer;
}
.remember-me label {
color: #666;
font-size: 0.95em;
cursor: pointer;
}
.btn-login {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 1.1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 20px;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
.btn-login:active {
transform: translateY(0);
}
.btn-login:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.error-message {
background: #fee;
color: #c33;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
border-left: 4px solid #c33;
}
.error-message.show {
display: block;
animation: shake 0.3s;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-10px); }
75% { transform: translateX(10px); }
}
.success-message {
background: #efe;
color: #3c3;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
border-left: 4px solid #3c3;
}
.success-message.show {
display: block;
}
.footer-links {
text-align: center;
margin-top: 20px;
}
.footer-links a {
color: #667eea;
text-decoration: none;
font-size: 0.9em;
margin: 0 10px;
}
.footer-links a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
.login-container {
flex-direction: column;
}
.login-left {
padding: 40px 30px;
}
.login-right {
padding: 40px 30px;
}
.login-left h1 {
font-size: 2em;
}
}
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-left">
<h1>🚀 Go Tunnel</h1>
<p>强大的端口转发与隧道管理系统</p>
<ul class="feature-list">
<li>实时流量监控</li>
<li>活跃连接管理</li>
<li>支持隧道模式</li>
<li>带宽限制控制</li>
<li>可视化管理界面</li>
</ul>
</div>
<div class="login-right">
<div class="login-header">
<h2>欢迎回来</h2>
<p>请输入您的 API Key 登录系统</p>
</div>
<div class="error-message" id="errorMessage"></div>
<div class="success-message" id="successMessage"></div>
<form id="loginForm">
<div class="form-group">
<label for="apiKey">API Key</label>
<input
type="password"
id="apiKey"
name="apiKey"
placeholder="请输入您的 API Key"
required
autocomplete="current-password"
>
</div>
<div class="remember-me">
<input type="checkbox" id="rememberMe" name="rememberMe">
<label for="rememberMe">记住我的 API Key</label>
</div>
<button type="submit" class="btn-login" id="loginBtn">
登录
</button>
</form>
<div class="footer-links">
<a href="#" onclick="showHelp(); return false;">需要帮助?</a>
<a href="/health" target="_blank">系统状态</a>
</div>
</div>
</div>
<script>
// 检查是否已登录
const savedApiKey = localStorage.getItem('apiKey');
if (savedApiKey) {
// 验证保存的 API Key 是否有效
verifyApiKey(savedApiKey).then(valid => {
if (valid) {
window.location.href = '/dashboard';
}
});
}
// 登录表单提交
document.getElementById('loginForm').addEventListener('submit', async (e) => {
e.preventDefault();
const apiKey = document.getElementById('apiKey').value.trim();
const rememberMe = document.getElementById('rememberMe').checked;
const loginBtn = document.getElementById('loginBtn');
const errorMsg = document.getElementById('errorMessage');
const successMsg = document.getElementById('successMessage');
if (!apiKey) {
showError('请输入 API Key');
return;
}
// 显示加载状态
loginBtn.disabled = true;
loginBtn.innerHTML = '<span class="loading"></span> 登录中...';
errorMsg.classList.remove('show');
successMsg.classList.remove('show');
try {
// 验证 API Key
const valid = await verifyApiKey(apiKey);
if (valid) {
// 保存 API Key
if (rememberMe) {
localStorage.setItem('apiKey', apiKey);
} else {
sessionStorage.setItem('apiKey', apiKey);
}
// 显示成功消息
successMsg.textContent = '登录成功!正在跳转...';
successMsg.classList.add('show');
// 跳转到管理界面
setTimeout(() => {
window.location.href = '/dashboard';
}, 1000);
} else {
showError('API Key 无效,请检查后重试');
loginBtn.disabled = false;
loginBtn.textContent = '登录';
}
} catch (error) {
showError('登录失败:' + error.message);
loginBtn.disabled = false;
loginBtn.textContent = '登录';
}
});
// 验证 API Key
async function verifyApiKey(apiKey) {
try {
const response = await fetch('/api/mapping/list', {
headers: {
'X-API-Key': apiKey
}
});
if (response.status === 401) {
return false;
}
if (response.ok) {
return true;
}
throw new Error('验证请求失败');
} catch (error) {
console.error('API Key 验证失败:', error);
return false;
}
}
// 显示错误消息
function showError(message) {
const errorMsg = document.getElementById('errorMessage');
errorMsg.textContent = message;
errorMsg.classList.add('show');
// 3秒后自动隐藏
setTimeout(() => {
errorMsg.classList.remove('show');
}, 3000);
}
// 显示帮助
function showHelp() {
alert('如需帮助,请联系系统管理员获取 API Key。\n\nAPI Key 可以在服务器配置文件中找到。');
}
// 回车键提交
document.getElementById('apiKey').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
document.getElementById('loginForm').dispatchEvent(new Event('submit'));
}
});
</script>
</body>
</html>

View File

@ -1,844 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>端口映射管理 - Go Tunnel</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.nav-tabs {
display: flex;
background: rgba(255, 255, 255, 0.1);
border-radius: 15px 15px 0 0;
overflow: hidden;
margin-bottom: 0;
}
.nav-tab {
flex: 1;
padding: 15px 20px;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.7);
cursor: pointer;
transition: all 0.3s;
font-size: 1.1em;
}
.nav-tab.active {
background: rgba(255, 255, 255, 0.95);
color: #667eea;
font-weight: bold;
}
.nav-tab:hover:not(.active) {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.tab-content {
background: rgba(255, 255, 255, 0.95);
border-radius: 0 0 15px 15px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
min-height: 600px;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: bold;
font-size: 1.1em;
}
.form-group input,
.form-group select {
width: 100%;
padding: 12px 15px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1em;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 10px rgba(102, 126, 234, 0.2);
}
.btn {
padding: 12px 25px;
border: none;
border-radius: 8px;
font-size: 1em;
cursor: pointer;
transition: all 0.3s;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-primary {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-danger {
background: linear-gradient(45deg, #ff6b6b, #ee5a52);
color: white;
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255, 107, 107, 0.4);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-2px);
}
.mapping-list {
max-height: 500px;
overflow-y: auto;
}
.mapping-item {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin-bottom: 15px;
border-left: 4px solid #667eea;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s;
}
.mapping-item:hover {
transform: translateX(5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.mapping-info {
flex: 1;
}
.mapping-info h4 {
color: #667eea;
margin-bottom: 8px;
font-size: 1.2em;
}
.mapping-details {
color: #666;
font-size: 0.95em;
line-height: 1.4;
}
.mapping-actions {
display: flex;
gap: 10px;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8em;
font-weight: bold;
text-transform: uppercase;
}
.status-tunnel {
background: #e3f2fd;
color: #1976d2;
}
.status-direct {
background: #f3e5f5;
color: #7b1fa2;
}
.message {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-weight: bold;
}
.message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.message.info {
background: #cce7ff;
color: #004085;
border: 1px solid #b3d7ff;
}
.loading {
text-align: center;
padding: 40px;
color: #666;
}
.spinner {
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.checkbox-group input[type="checkbox"] {
width: auto;
transform: scale(1.2);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
}
.stat-value {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9em;
opacity: 0.9;
}
.quick-actions {
display: flex;
gap: 15px;
margin-bottom: 20px;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.form-grid {
grid-template-columns: 1fr;
}
.mapping-item {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.mapping-actions {
width: 100%;
justify-content: flex-end;
}
.quick-actions {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 端口映射管理中心</h1>
<div class="nav-tabs">
<button class="nav-tab active" onclick="switchTab('overview')">概览</button>
<button class="nav-tab" onclick="switchTab('create')">创建映射</button>
<button class="nav-tab" onclick="switchTab('manage')">管理映射</button>
<button class="nav-tab" onclick="switchTab('monitor')">流量监控</button>
</div>
<div class="tab-content">
<!-- 概览页面 -->
<div id="overview" class="tab-pane active">
<h2 style="color: #667eea; margin-bottom: 20px;">系统概览</h2>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="total-mappings">-</div>
<div class="stat-label">总映射数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="tunnel-mappings">-</div>
<div class="stat-label">隧道映射</div>
</div>
<div class="stat-card">
<div class="stat-value" id="direct-mappings">-</div>
<div class="stat-label">直连映射</div>
</div>
<div class="stat-card">
<div class="stat-value" id="tunnel-status">-</div>
<div class="stat-label">隧道状态</div>
</div>
</div>
<div class="quick-actions">
<button class="btn btn-primary" onclick="switchTab('create')">
快速创建映射
</button>
<button class="btn btn-secondary" onclick="refreshOverview()">
🔄 刷新状态
</button>
<button class="btn btn-secondary" onclick="switchTab('monitor')">
📊 查看监控
</button>
</div>
<div id="recent-mappings">
<h3 style="color: #667eea; margin-bottom: 15px;">最近的映射</h3>
<div id="recent-mappings-list" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
</div>
</div>
<!-- 创建映射页面 -->
<div id="create" class="tab-pane">
<h2 style="color: #667eea; margin-bottom: 20px;">创建新的端口映射</h2>
<div id="create-message"></div>
<form id="create-form">
<div class="form-grid">
<div class="form-group">
<label for="source-port">源端口 (本地监听端口) *</label>
<input type="number" id="source-port" min="1" max="65535" required placeholder="例如: 8080">
</div>
<div class="form-group">
<label for="target-host">目标主机 *</label>
<input type="text" id="target-host" required placeholder="例如: localhost 或 192.168.1.100">
</div>
<div class="form-group">
<label for="target-port">目标端口 *</label>
<input type="number" id="target-port" min="1" max="65535" required placeholder="例如: 3000">
</div>
<div class="form-group">
<label for="bandwidth-limit">带宽限制 (可选)</label>
<input type="number" id="bandwidth-limit" min="0" placeholder="例如: 1048576 (1MB/s)">
<small style="color: #666; margin-top: 5px; display: block;">
限制此映射的传输速度,单位:字节/秒。留空表示不限制<br>
示例1048576 = 1MB/s, 10485760 = 10MB/s
</small>
</div>
<div class="form-group">
<label>连接模式</label>
<div class="checkbox-group">
<input type="checkbox" id="use-tunnel">
<label for="use-tunnel">使用隧道模式</label>
</div>
<small style="color: #666; margin-top: 5px; display: block;">
隧道模式:通过加密隧道转发流量,适合跨网络访问<br>
直连模式直接TCP转发适合本地网络访问
</small>
</div>
</div>
<div style="text-align: center; margin-top: 30px;">
<button type="submit" class="btn btn-primary" style="margin-right: 15px;">
🚀 创建映射
</button>
<button type="button" class="btn btn-secondary" onclick="resetCreateForm()">
🔄 重置表单
</button>
</div>
</form>
</div>
<!-- 管理映射页面 -->
<div id="manage" class="tab-pane">
<h2 style="color: #667eea; margin-bottom: 20px;">管理端口映射</h2>
<div id="manage-message"></div>
<div style="text-align: right; margin-bottom: 20px;">
<button class="btn btn-secondary" onclick="loadMappings()">
🔄 刷新列表
</button>
</div>
<div id="mappings-list" class="loading">
<div class="spinner"></div>
<p>加载映射列表中...</p>
</div>
</div>
<!-- 流量监控页面 -->
<div id="monitor" class="tab-pane">
<h2 style="color: #667eea; margin-bottom: 20px;">流量监控</h2>
<div style="text-align: center; padding: 40px;">
<p style="color: #666; margin-bottom: 20px;">
点击下方按钮打开详细的流量监控页面
</p>
<button class="btn btn-primary" onclick="openTrafficMonitor()">
📊 打开流量监控
</button>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let currentMappings = [];
let apiKey = '';
// 格式化带宽限制为可读格式
function formatBandwidth(bytes) {
if (!bytes || bytes === 0) {
return '无限制';
}
if (bytes < 1024) {
return bytes + ' B/s';
} else if (bytes < 1024 * 1024) {
return (bytes / 1024).toFixed(2) + ' KB/s';
} else if (bytes < 1024 * 1024 * 1024) {
return (bytes / (1024 * 1024)).toFixed(2) + ' MB/s';
} else {
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB/s';
}
}
// 检查是否已有 API Key
function checkAuth() {
const savedKey = sessionStorage.getItem('apiKey');
if (savedKey) {
apiKey = savedKey;
showMainContent();
} else {
showAuthDialog();
}
}
// 显示认证对话框
function showAuthDialog() {
const authHTML = '' +
'<div style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; ' +
' background: rgba(0,0,0,0.5); display: flex; align-items: center; ' +
' justify-content: center; z-index: 9999;">' +
' <div style="background: white; padding: 40px; border-radius: 15px; ' +
' box-shadow: 0 10px 40px rgba(0,0,0,0.3); max-width: 400px; width: 90%;">' +
' <h2 style="color: #667eea; margin-bottom: 20px; text-align: center;">' +
' 🔐 请输入访问密钥' +
' </h2>' +
' <div id="auth-error" style="display: none;" class="message error"></div>' +
' <form id="auth-form">' +
' <div class="form-group">' +
' <label for="secret-input">API Key / Secret</label>' +
' <input type="password" id="secret-input" required ' +
' placeholder="请输入您的访问密钥"' +
' style="width: 100%; padding: 12px; border: 2px solid #ddd; ' +
' border-radius: 8px; font-size: 1em;">' +
' </div>' +
' <button type="submit" class="btn btn-primary" ' +
' style="width: 100%; margin-top: 20px;">' +
' 验证并进入' +
' </button>' +
' </form>' +
' </div>' +
'</div>';
document.body.insertAdjacentHTML('beforeend', authHTML);
document.getElementById('auth-form').addEventListener('submit', async function (e) {
e.preventDefault();
const secretInput = document.getElementById('secret-input');
const secret = secretInput.value.trim();
if (!secret) {
return;
}
// 验证 API Key
try {
const response = await fetch('/health?api_key=' + encodeURIComponent(secret));
if (response.ok) {
// 验证成功
apiKey = secret;
sessionStorage.setItem('apiKey', secret);
document.querySelector('[style*="position: fixed"]').remove();
showMainContent();
} else {
// 验证失败
const errorDiv = document.getElementById('auth-error');
errorDiv.textContent = '❌ 访问密钥无效,请重试';
errorDiv.style.display = 'block';
secretInput.value = '';
secretInput.focus();
}
} catch (error) {
const errorDiv = document.getElementById('auth-error');
errorDiv.textContent = '❌ 验证失败: ' + error.message;
errorDiv.style.display = 'block';
}
});
document.getElementById('secret-input').focus();
}
// 显示主内容
function showMainContent() {
document.querySelector('.container').style.display = 'block';
refreshOverview();
}
// 获取带 API Key 的 URL
function getApiUrl(url) {
const separator = url.includes('?') ? '&' : '?';
return url + separator + 'api_key=' + encodeURIComponent(apiKey);
}
// 切换标签页
function switchTab(tabName) {
// 隐藏所有标签页内容
document.querySelectorAll('.tab-pane').forEach(pane => {
pane.classList.remove('active');
});
// 移除所有标签页按钮的激活状态
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.classList.remove('active');
});
// 显示选中的标签页
document.getElementById(tabName).classList.add('active');
event.target.classList.add('active');
// 根据标签页执行相应的初始化
switch (tabName) {
case 'overview':
refreshOverview();
break;
case 'manage':
loadMappings();
break;
}
}
// 显示消息
function showMessage(containerId, type, message) {
const container = document.getElementById(containerId);
container.innerHTML = '<div class="message ' + type + '">' + message + '</div>';
// 3秒后自动隐藏成功消息
if (type === 'success') {
setTimeout(() => {
container.innerHTML = '';
}, 3000);
}
}
// 创建映射表单提交
document.getElementById('create-form').addEventListener('submit', async function (e) {
e.preventDefault();
const bandwidthLimitValue = document.getElementById('bandwidth-limit').value;
const formData = {
source_port: parseInt(document.getElementById('source-port').value),
target_host: document.getElementById('target-host').value,
target_port: parseInt(document.getElementById('target-port').value),
use_tunnel: document.getElementById('use-tunnel').checked
};
// 如果填写了带宽限制,则添加到请求中
if (bandwidthLimitValue && bandwidthLimitValue.trim() !== '') {
formData.bandwidth_limit = parseInt(bandwidthLimitValue);
}
try {
const response = await fetch(getApiUrl('/api/mapping/create'), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
showMessage('create-message', 'success', '✅ ' + result.message);
resetCreateForm();
} else {
showMessage('create-message', 'error', '❌ ' + result.message);
}
} catch (error) {
showMessage('create-message', 'error', '❌ 网络错误: ' + error.message);
}
});
// 重置创建表单
function resetCreateForm() {
document.getElementById('create-form').reset();
}
// 删除映射
async function deleteMapping(port) {
if (!confirm('确定要删除端口 ' + port + ' 的映射吗?')) {
return;
}
try {
const response = await fetch(getApiUrl('/api/mapping/remove'), {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ port: port })
});
const result = await response.json();
if (result.success) {
showMessage('manage-message', 'success', '✅ ' + result.message);
loadMappings(); // 重新加载列表
} else {
showMessage('manage-message', 'error', '❌ ' + result.message);
}
} catch (error) {
showMessage('manage-message', 'error', '❌ 网络错误: ' + error.message);
}
}
// 加载映射列表
async function loadMappings() {
const container = document.getElementById('mappings-list');
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
try {
const response = await fetch(getApiUrl('/api/mapping/list'));
const result = await response.json();
if (result.success) {
currentMappings = result.data.mappings || [];
renderMappings(currentMappings);
} else {
container.innerHTML = '<div class="message error">❌ ' + result.message + '</div>';
}
} catch (error) {
container.innerHTML = '<div class="message error">❌ 网络错误: ' + error.message + '</div>';
}
}
// 渲染映射列表
function renderMappings(mappings) {
const container = document.getElementById('mappings-list');
if (mappings.length === 0) {
container.innerHTML = '<div class="message info">📝 暂无端口映射,点击"创建映射"标签页开始创建。</div>';
return;
}
const html = mappings.map(mapping =>
'<div class="mapping-item">' +
'<div class="mapping-info">' +
'<h4>端口 ' + mapping.source_port + '</h4>' +
'<div class="mapping-details">' +
'<strong>目标:</strong> ' + mapping.target_host + ':' + mapping.target_port + '<br>' +
'<strong>模式:</strong> ' +
'<span class="status-badge ' + (mapping.use_tunnel ? 'status-tunnel' : 'status-direct') + '">' +
(mapping.use_tunnel ? '隧道模式' : '直连模式') +
'</span><br>' +
'<strong>带宽限制:</strong> ' + formatBandwidth(mapping.bandwidth_limit) + '<br>' +
'<strong>创建时间:</strong> ' + new Date(mapping.created_at).toLocaleString('zh-CN') +
'</div>' +
'</div>' +
'<div class="mapping-actions">' +
'<button class="btn btn-danger" onclick="deleteMapping(' + mapping.source_port + ')">' +
'🗑️ 删除' +
'</button>' +
'</div>' +
'</div>'
).join('');
container.innerHTML = html;
}
// 刷新概览页面
async function refreshOverview() {
try {
// 加载映射列表
const mappingsResponse = await fetch(getApiUrl('/api/mapping/list'));
const mappingsResult = await mappingsResponse.json();
// 加载健康状态
const healthResponse = await fetch(getApiUrl('/health'));
const healthResult = await healthResponse.json();
if (mappingsResult.success) {
const mappings = mappingsResult.data.mappings || [];
const tunnelMappings = mappings.filter(m => m.use_tunnel).length;
const directMappings = mappings.filter(m => !m.use_tunnel).length;
document.getElementById('total-mappings').textContent = mappings.length;
document.getElementById('tunnel-mappings').textContent = tunnelMappings;
document.getElementById('direct-mappings').textContent = directMappings;
// 渲染最近的映射最多5个
const recentMappings = mappings.slice(-5).reverse();
renderRecentMappings(recentMappings);
}
if (healthResult) {
const tunnelStatus = healthResult.tunnel_enabled ?
(healthResult.tunnel_connected ? '🟢 已连接' : '🟡 未连接') :
'🔴 未启用';
document.getElementById('tunnel-status').textContent = tunnelStatus;
}
} catch (error) {
console.error('刷新概览失败:', error);
}
}
// 渲染最近的映射
function renderRecentMappings(mappings) {
const container = document.getElementById('recent-mappings-list');
if (mappings.length === 0) {
container.innerHTML = '<div class="message info">暂无映射记录</div>';
return;
}
const html = mappings.map(mapping =>
'<div class="mapping-item">' +
'<div class="mapping-info">' +
'<h4>端口 ' + mapping.source_port + '</h4>' +
'<div class="mapping-details">' +
'<strong>目标:</strong> ' + mapping.target_host + ':' + mapping.target_port + ' | ' +
'<span class="status-badge ' + (mapping.use_tunnel ? 'status-tunnel' : 'status-direct') + '">' +
(mapping.use_tunnel ? '隧道' : '直连') +
'</span> | ' +
'<strong>带宽:</strong> ' + formatBandwidth(mapping.bandwidth_limit) +
'</div>' +
'</div>' +
'</div>'
).join('');
container.innerHTML = html;
}
// 打开流量监控页面
function openTrafficMonitor() {
window.open(getApiUrl('/api/stats/monitor'), '_blank');
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function () {
// 隐藏主内容,等待认证
document.querySelector('.container').style.display = 'none';
checkAuth();
});
</script>
</body>
</html>

View File

@ -3,7 +3,6 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>流量监控 - Go Tunnel</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
* {

View File

@ -17,9 +17,34 @@ type PortTrafficStats struct {
// AllTrafficStats 所有流量统计
type AllTrafficStats struct {
Tunnel TrafficStats `json:"tunnel"` // 隧道流量
Mappings []PortTrafficStats `json:"mappings"` // 端口映射流量
TotalSent uint64 `json:"total_sent"` // 总发送
TotalReceived uint64 `json:"total_received"` // 总接收
Timestamp int64 `json:"timestamp"` // 时间戳
Tunnel TrafficStats `json:"tunnel"` // 隧道流量
Mappings []PortTrafficStats `json:"mappings"` // 端口映射流量
TotalSent uint64 `json:"total_sent"` // 总发送
TotalReceived uint64 `json:"total_received"` // 总接收
Timestamp int64 `json:"timestamp"` // 时间戳
}
// ConnectionInfo 连接信息
type ConnectionInfo struct {
ClientAddr string `json:"client_addr"` // 客户端地址
TargetAddr string `json:"target_addr"` // 目标地址
BytesSent uint64 `json:"bytes_sent"` // 该连接发送的字节数
BytesReceived uint64 `json:"bytes_received"` // 该连接接收的字节数
ConnectedAt int64 `json:"connected_at"` // 连接建立时间Unix时间戳
}
// PortConnectionStats 端口连接统计
type PortConnectionStats struct {
SourcePort int `json:"source_port"` // 源端口
TargetHost string `json:"target_host"` // 目标主机
TargetPort int `json:"target_port"` // 目标端口
UseTunnel bool `json:"use_tunnel"` // 是否使用隧道
ActiveConnections []ConnectionInfo `json:"active_connections"` // 活跃连接列表
TotalConnections int `json:"total_connections"` // 总连接数
}
// AllConnectionsStats 所有连接统计
type AllConnectionsStats struct {
Mappings []PortConnectionStats `json:"mappings"` // 所有映射的连接信息
Timestamp int64 `json:"timestamp"` // 时间戳
}