server use sqlite
All checks were successful
Build Multi-Platform Binaries / build (push) Successful in 11m35s

This commit is contained in:
Flik
2025-12-25 19:48:11 +08:00
parent 790d3b682a
commit 7100362cd7
23 changed files with 688 additions and 421 deletions

View File

@@ -0,0 +1,177 @@
package tunnel
import (
"fmt"
"log"
"net"
"sync"
"time"
"github.com/gotunnel/pkg/protocol"
"github.com/gotunnel/pkg/relay"
"github.com/google/uuid"
"github.com/hashicorp/yamux"
)
// Client 隧道客户端
type Client struct {
ServerAddr string
Token string
ID string
session *yamux.Session
rules []protocol.ProxyRule
mu sync.RWMutex
}
// NewClient 创建客户端
func NewClient(serverAddr, token, id string) *Client {
if id == "" {
id = uuid.New().String()[:8]
}
return &Client{
ServerAddr: serverAddr,
Token: token,
ID: id,
}
}
// Run 启动客户端(带断线重连)
func (c *Client) Run() error {
for {
if err := c.connect(); err != nil {
log.Printf("[Client] Connect error: %v", err)
log.Printf("[Client] Reconnecting in 5s...")
time.Sleep(5 * time.Second)
continue
}
c.handleSession()
log.Printf("[Client] Disconnected, reconnecting...")
time.Sleep(3 * time.Second)
}
}
// connect 连接到服务端并认证
func (c *Client) connect() error {
conn, err := net.DialTimeout("tcp", c.ServerAddr, 10*time.Second)
if err != nil {
return err
}
authReq := protocol.AuthRequest{ClientID: c.ID, Token: c.Token}
msg, _ := protocol.NewMessage(protocol.MsgTypeAuth, authReq)
if err := protocol.WriteMessage(conn, msg); err != nil {
conn.Close()
return err
}
resp, err := protocol.ReadMessage(conn)
if err != nil {
conn.Close()
return err
}
var authResp protocol.AuthResponse
resp.ParsePayload(&authResp)
if !authResp.Success {
conn.Close()
return fmt.Errorf("auth failed: %s", authResp.Message)
}
log.Printf("[Client] Authenticated as %s", c.ID)
session, err := yamux.Client(conn, nil)
if err != nil {
conn.Close()
return err
}
c.mu.Lock()
c.session = session
c.mu.Unlock()
return nil
}
// handleSession 处理会话
func (c *Client) handleSession() {
defer c.session.Close()
for {
stream, err := c.session.Accept()
if err != nil {
return
}
go c.handleStream(stream)
}
}
// handleStream 处理流
func (c *Client) handleStream(stream net.Conn) {
defer stream.Close()
msg, err := protocol.ReadMessage(stream)
if err != nil {
return
}
switch msg.Type {
case protocol.MsgTypeProxyConfig:
c.handleProxyConfig(msg)
case protocol.MsgTypeNewProxy:
c.handleNewProxy(stream, msg)
case protocol.MsgTypeHeartbeat:
c.handleHeartbeat(stream)
}
}
// handleProxyConfig 处理代理配置
func (c *Client) handleProxyConfig(msg *protocol.Message) {
var cfg protocol.ProxyConfig
msg.ParsePayload(&cfg)
c.mu.Lock()
c.rules = cfg.Rules
c.mu.Unlock()
log.Printf("[Client] Received %d proxy rules", len(cfg.Rules))
for _, r := range cfg.Rules {
log.Printf("[Client] %s: %s:%d", r.Name, r.LocalIP, r.LocalPort)
}
}
// handleNewProxy 处理新代理请求
func (c *Client) handleNewProxy(stream net.Conn, msg *protocol.Message) {
var req protocol.NewProxyRequest
msg.ParsePayload(&req)
var rule *protocol.ProxyRule
c.mu.RLock()
for _, r := range c.rules {
if r.RemotePort == req.RemotePort {
rule = &r
break
}
}
c.mu.RUnlock()
if rule == nil {
log.Printf("[Client] Unknown port %d", req.RemotePort)
return
}
localAddr := fmt.Sprintf("%s:%d", rule.LocalIP, rule.LocalPort)
localConn, err := net.DialTimeout("tcp", localAddr, 5*time.Second)
if err != nil {
log.Printf("[Client] Connect %s error: %v", localAddr, err)
return
}
relay.Relay(stream, localConn)
}
// handleHeartbeat 处理心跳
func (c *Client) handleHeartbeat(stream net.Conn) {
msg := &protocol.Message{Type: protocol.MsgTypeHeartbeatAck}
protocol.WriteMessage(stream, msg)
}

112
internal/server/app/app.go Normal file
View File

@@ -0,0 +1,112 @@
package app
import (
"embed"
"io"
"io/fs"
"log"
"net/http"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/internal/server/router"
)
//go:embed dist/*
var staticFiles embed.FS
// spaHandler SPA路由处理器
type spaHandler struct {
fs http.FileSystem
}
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
f, err := h.fs.Open(path)
if err != nil {
f, err = h.fs.Open("index.html")
if err != nil {
http.Error(w, "Not Found", http.StatusNotFound)
return
}
}
defer f.Close()
stat, _ := f.Stat()
if stat.IsDir() {
f, err = h.fs.Open(path + "/index.html")
if err != nil {
f, _ = h.fs.Open("index.html")
}
}
http.ServeContent(w, r, path, stat.ModTime(), f.(io.ReadSeeker))
}
// WebServer Web控制台服务
type WebServer struct {
ClientStore db.ClientStore
Server router.ServerInterface
}
// NewWebServer 创建Web服务
func NewWebServer(cs db.ClientStore, srv router.ServerInterface) *WebServer {
return &WebServer{
ClientStore: cs,
Server: srv,
}
}
// Run 启动Web服务
func (w *WebServer) Run(addr string) error {
r := router.New()
router.RegisterRoutes(r, w)
staticFS, err := fs.Sub(staticFiles, "dist")
if err != nil {
return err
}
r.Handle("/", spaHandler{fs: http.FS(staticFS)})
log.Printf("[Web] Console listening on %s", addr)
return http.ListenAndServe(addr, r.Handler())
}
// RunWithAuth 启动带认证的Web服务
func (w *WebServer) RunWithAuth(addr, username, password string) error {
r := router.New()
router.RegisterRoutes(r, w)
staticFS, err := fs.Sub(staticFiles, "dist")
if err != nil {
return err
}
r.Handle("/", spaHandler{fs: http.FS(staticFS)})
handler := &authMiddleware{username, password, r.Handler()}
log.Printf("[Web] Console listening on %s (auth enabled)", addr)
return http.ListenAndServe(addr, handler)
}
type authMiddleware struct {
username, password string
handler http.Handler
}
func (a *authMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok || user != a.username || pass != a.password {
w.Header().Set("WWW-Authenticate", `Basic realm="GoTunnel"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
a.handler.ServeHTTP(w, r)
}
// GetClientStore 获取客户端存储
func (w *WebServer) GetClientStore() db.ClientStore {
return w.ClientStore
}
// GetServer 获取服务端接口
func (w *WebServer) GetServer() router.ServerInterface {
return w.Server
}

View File

@@ -0,0 +1 @@
import{d as D,j as F,r,o as I,c as n,a as e,b as p,k as C,u as S,t as s,n as J,F as V,e as g,l as M,m as O,p as P,f as _,v,i as o,_ as $}from"./index-BvqIwKwu.js";const j={class:"client-view"},z={class:"header"},L={key:0,class:"ping-info"},T={class:"rules-section"},q={class:"section-header"},A={key:0},G={key:0,class:"rules-table"},H={key:1,class:"edit-form"},K=["onUpdate:modelValue"],Q=["onUpdate:modelValue"],W=["onUpdate:modelValue"],X=["onUpdate:modelValue"],Y=["onClick"],Z=D({__name:"ClientView",setup(ee){const w=F(),f=S(),u=w.params.id,m=r(!1),h=r(""),b=r([]),i=r(!1),d=r([]),y=async()=>{try{const{data:l}=await M(u);m.value=l.online,h.value=l.last_ping||"",b.value=l.rules||[]}catch(l){console.error("Failed to load client",l)}};I(y);const U=()=>{d.value=JSON.parse(JSON.stringify(b.value)),i.value=!0},R=()=>{i.value=!1},x=()=>{d.value.push({name:"",local_ip:"127.0.0.1",local_port:80,remote_port:8080})},E=l=>{d.value.splice(l,1)},N=async()=>{try{await O(u,{id:u,rules:d.value}),i.value=!1,y()}catch{alert("保存失败")}},B=async()=>{if(confirm("确定删除此客户端?"))try{await P(u),f.push("/")}catch{alert("删除失败")}};return(l,c)=>(o(),n("div",j,[e("div",z,[e("button",{class:"btn",onClick:c[0]||(c[0]=t=>C(f).push("/"))},"← 返回"),e("h2",null,s(C(u)),1),e("span",{class:J(["status-badge",m.value?"online":"offline"])},s(m.value?"在线":"离线"),3)]),h.value?(o(),n("div",L,"最后心跳: "+s(h.value),1)):p("",!0),e("div",T,[e("div",q,[c[1]||(c[1]=e("h3",null,"代理规则",-1)),i.value?p("",!0):(o(),n("div",A,[e("button",{class:"btn primary",onClick:U},"编辑"),e("button",{class:"btn danger",onClick:B},"删除")]))]),i.value?p("",!0):(o(),n("table",G,[c[2]||(c[2]=e("thead",null,[e("tr",null,[e("th",null,"名称"),e("th",null,"本地地址"),e("th",null,"远程端口")])],-1)),e("tbody",null,[(o(!0),n(V,null,g(b.value,t=>(o(),n("tr",{key:t.name},[e("td",null,s(t.name),1),e("td",null,s(t.local_ip)+":"+s(t.local_port),1),e("td",null,s(t.remote_port),1)]))),128))])])),i.value?(o(),n("div",H,[(o(!0),n(V,null,g(d.value,(t,k)=>(o(),n("div",{key:k,class:"rule-row"},[_(e("input",{"onUpdate:modelValue":a=>t.name=a,placeholder:"名称"},null,8,K),[[v,t.name]]),_(e("input",{"onUpdate:modelValue":a=>t.local_ip=a,placeholder:"本地IP"},null,8,Q),[[v,t.local_ip]]),_(e("input",{"onUpdate:modelValue":a=>t.local_port=a,type:"number",placeholder:"本地端口"},null,8,W),[[v,t.local_port,void 0,{number:!0}]]),_(e("input",{"onUpdate:modelValue":a=>t.remote_port=a,type:"number",placeholder:"远程端口"},null,8,X),[[v,t.remote_port,void 0,{number:!0}]]),e("button",{class:"btn-icon",onClick:a=>E(k)},"×",8,Y)]))),128)),e("button",{class:"btn secondary",onClick:x},"+ 添加规则"),e("div",{class:"edit-actions"},[e("button",{class:"btn",onClick:R},"取消"),e("button",{class:"btn primary",onClick:N},"保存")])])):p("",!0)])]))}}),le=$(Z,[["__scopeId","data-v-01b16887"]]);export{le as default};

View File

@@ -0,0 +1 @@
.header[data-v-01b16887]{display:flex;align-items:center;gap:16px;margin-bottom:20px}.header h2[data-v-01b16887]{margin:0}.status-badge[data-v-01b16887]{padding:4px 12px;border-radius:12px;font-size:12px}.status-badge.online[data-v-01b16887]{background:#d4edda;color:#155724}.status-badge.offline[data-v-01b16887]{background:#f8d7da;color:#721c24}.ping-info[data-v-01b16887]{color:#666;margin-bottom:20px}.rules-section[data-v-01b16887]{background:#fff;border-radius:8px;padding:20px;box-shadow:0 2px 4px #0000001a}.section-header[data-v-01b16887]{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.section-header h3[data-v-01b16887]{margin:0}.section-header .btn[data-v-01b16887]{margin-left:8px}.btn[data-v-01b16887]{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;font-size:14px}.btn.primary[data-v-01b16887]{background:#3498db;color:#fff}.btn.secondary[data-v-01b16887]{background:#95a5a6;color:#fff}.btn.danger[data-v-01b16887]{background:#e74c3c;color:#fff}.rules-table[data-v-01b16887]{width:100%;border-collapse:collapse}.rules-table th[data-v-01b16887],.rules-table td[data-v-01b16887]{padding:12px;text-align:left;border-bottom:1px solid #eee}.rules-table th[data-v-01b16887]{font-weight:600}.rule-row[data-v-01b16887]{display:flex;gap:8px;margin-bottom:8px}.rule-row input[data-v-01b16887]{flex:1;padding:8px;border:1px solid #ddd;border-radius:4px}.btn-icon[data-v-01b16887]{background:#e74c3c;color:#fff;border:none;border-radius:4px;width:32px;cursor:pointer}.edit-actions[data-v-01b16887]{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}

View File

@@ -0,0 +1 @@
import{d as M,r as p,o as $,c as n,a as e,b as f,F as h,e as b,w as x,f as u,v as c,g as I,h as R,u as B,i as a,t as C,n as D,_ as F}from"./index-BvqIwKwu.js";const H={class:"home"},N={class:"client-grid"},z=["onClick"],A={class:"card-header"},E={class:"client-id"},L={class:"card-info"},P={key:0,class:"empty"},S={class:"modal"},T={class:"form-group"},j={class:"form-group"},q=["onUpdate:modelValue"],G=["onUpdate:modelValue"],J=["onUpdate:modelValue"],K=["onUpdate:modelValue"],O=["onClick"],Q={class:"modal-actions"},W=M({__name:"HomeView",setup(X){const y=B(),v=p([]),d=p(!1),i=p(""),r=p([]),m=async()=>{try{const{data:t}=await I();v.value=t||[]}catch(t){console.error("Failed to load clients",t)}};$(m);const k=()=>{i.value="",r.value=[{name:"",local_ip:"127.0.0.1",local_port:80,remote_port:8080}],d.value=!0},V=()=>{r.value.push({name:"",local_ip:"127.0.0.1",local_port:80,remote_port:8080})},w=t=>{r.value.splice(t,1)},U=async()=>{if(i.value)try{await R({id:i.value,rules:r.value}),d.value=!1,m()}catch{alert("添加失败")}},g=t=>{y.push(`/client/${t}`)};return(t,l)=>(a(),n("div",H,[e("div",{class:"toolbar"},[l[3]||(l[3]=e("h2",null,"客户端列表",-1)),e("button",{class:"btn primary",onClick:k},"添加客户端")]),e("div",N,[(a(!0),n(h,null,b(v.value,o=>(a(),n("div",{key:o.id,class:"client-card",onClick:_=>g(o.id)},[e("div",A,[e("span",E,C(o.id),1),e("span",{class:D(["status",o.online?"online":"offline"])},null,2)]),e("div",L,[e("span",null,C(o.rule_count)+" 条规则",1)])],8,z))),128))]),v.value.length===0?(a(),n("div",P,"暂无客户端配置")):f("",!0),d.value?(a(),n("div",{key:1,class:"modal-overlay",onClick:l[2]||(l[2]=x(o=>d.value=!1,["self"]))},[e("div",S,[l[6]||(l[6]=e("h3",null,"添加客户端",-1)),e("div",T,[l[4]||(l[4]=e("label",null,"客户端 ID",-1)),u(e("input",{"onUpdate:modelValue":l[0]||(l[0]=o=>i.value=o),placeholder:"例如: client-a"},null,512),[[c,i.value]])]),e("div",j,[l[5]||(l[5]=e("label",null,"代理规则",-1)),(a(!0),n(h,null,b(r.value,(o,_)=>(a(),n("div",{key:_,class:"rule-row"},[u(e("input",{"onUpdate:modelValue":s=>o.name=s,placeholder:"名称"},null,8,q),[[c,o.name]]),u(e("input",{"onUpdate:modelValue":s=>o.local_ip=s,placeholder:"本地IP"},null,8,G),[[c,o.local_ip]]),u(e("input",{"onUpdate:modelValue":s=>o.local_port=s,type:"number",placeholder:"本地端口"},null,8,J),[[c,o.local_port,void 0,{number:!0}]]),u(e("input",{"onUpdate:modelValue":s=>o.remote_port=s,type:"number",placeholder:"远程端口"},null,8,K),[[c,o.remote_port,void 0,{number:!0}]]),e("button",{class:"btn-icon",onClick:s=>w(_)},"×",8,O)]))),128)),e("button",{class:"btn secondary",onClick:V},"+ 添加规则")]),e("div",Q,[e("button",{class:"btn",onClick:l[1]||(l[1]=o=>d.value=!1)},"取消"),e("button",{class:"btn primary",onClick:U},"保存")])])])):f("",!0)]))}}),Z=F(W,[["__scopeId","data-v-fd6e4f1d"]]);export{Z as default};

View File

@@ -0,0 +1 @@
.toolbar[data-v-fd6e4f1d]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.toolbar h2[data-v-fd6e4f1d]{font-size:18px;color:#2c3e50}.btn[data-v-fd6e4f1d]{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;font-size:14px}.btn.primary[data-v-fd6e4f1d]{background:#3498db;color:#fff}.btn.secondary[data-v-fd6e4f1d]{background:#95a5a6;color:#fff}.client-grid[data-v-fd6e4f1d]{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}.client-card[data-v-fd6e4f1d]{background:#fff;border-radius:8px;padding:16px;cursor:pointer;box-shadow:0 2px 4px #0000001a;transition:transform .2s}.client-card[data-v-fd6e4f1d]:hover{transform:translateY(-2px)}.card-header[data-v-fd6e4f1d]{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}.client-id[data-v-fd6e4f1d]{font-weight:600}.status[data-v-fd6e4f1d]{width:10px;height:10px;border-radius:50%}.status.online[data-v-fd6e4f1d]{background:#27ae60}.status.offline[data-v-fd6e4f1d]{background:#95a5a6}.card-info[data-v-fd6e4f1d]{font-size:14px;color:#666}.empty[data-v-fd6e4f1d]{text-align:center;color:#999;padding:40px}.modal-overlay[data-v-fd6e4f1d]{position:fixed;inset:0;background:#00000080;display:flex;align-items:center;justify-content:center}.modal[data-v-fd6e4f1d]{background:#fff;border-radius:8px;padding:24px;width:500px;max-width:90%}.modal h3[data-v-fd6e4f1d],.form-group[data-v-fd6e4f1d]{margin-bottom:16px}.form-group label[data-v-fd6e4f1d]{display:block;margin-bottom:8px;font-weight:500}.form-group input[data-v-fd6e4f1d]{width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;box-sizing:border-box}.rule-row[data-v-fd6e4f1d]{display:flex;gap:8px;margin-bottom:8px}.rule-row input[data-v-fd6e4f1d]{flex:1;width:auto}.btn-icon[data-v-fd6e4f1d]{background:#e74c3c;color:#fff;border:none;border-radius:4px;width:32px;cursor:pointer}.modal-actions[data-v-fd6e4f1d]{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}.card{padding:2em}#app{max-width:1280px;margin:0 auto;padding:2rem;text-align:center}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}.app[data-v-dc56de06]{min-height:100vh;background:#f5f7fa}.header[data-v-dc56de06]{background:#fff;padding:16px 24px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 4px #0000001a}.header h1[data-v-dc56de06]{font-size:20px;color:#2c3e50}.server-info[data-v-dc56de06]{display:flex;align-items:center;gap:12px;color:#666}.badge[data-v-dc56de06]{background:#3498db;color:#fff;padding:4px 12px;border-radius:12px;font-size:12px}.main[data-v-dc56de06]{padding:24px;max-width:1200px;margin:0 auto}

14
internal/server/app/dist/index.html vendored Normal file
View File

@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title>
<script type="module" crossorigin src="/assets/index-BvqIwKwu.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-fTDfeMRP.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

1
internal/server/app/dist/vite.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,72 @@
package config
import (
"os"
"gopkg.in/yaml.v3"
)
// ServerConfig 服务端配置
type ServerConfig struct {
Server ServerSettings `yaml:"server"`
Web WebSettings `yaml:"web"`
}
// ServerSettings 服务端设置
type ServerSettings struct {
BindAddr string `yaml:"bind_addr"`
BindPort int `yaml:"bind_port"`
Token string `yaml:"token"`
HeartbeatSec int `yaml:"heartbeat_sec"`
HeartbeatTimeout int `yaml:"heartbeat_timeout"`
DBPath string `yaml:"db_path"`
}
// WebSettings Web控制台设置
type WebSettings struct {
Enabled bool `yaml:"enabled"`
BindAddr string `yaml:"bind_addr"`
BindPort int `yaml:"bind_port"`
Username string `yaml:"username"`
Password string `yaml:"password"`
}
// LoadServerConfig 加载服务端配置
func LoadServerConfig(path string) (*ServerConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var cfg ServerConfig
if err := yaml.Unmarshal(data, &cfg); err != nil {
return nil, err
}
// 设置默认值
if cfg.Server.BindAddr == "" {
cfg.Server.BindAddr = "0.0.0.0"
}
if cfg.Server.BindPort == 0 {
cfg.Server.BindPort = 7000
}
if cfg.Server.HeartbeatSec == 0 {
cfg.Server.HeartbeatSec = 30
}
if cfg.Server.HeartbeatTimeout == 0 {
cfg.Server.HeartbeatTimeout = 90
}
if cfg.Server.DBPath == "" {
cfg.Server.DBPath = "gotunnel.db"
}
// Web 默认值
if cfg.Web.BindAddr == "" {
cfg.Web.BindAddr = "0.0.0.0"
}
if cfg.Web.BindPort == 0 {
cfg.Web.BindPort = 7500
}
return &cfg, nil
}

View File

@@ -0,0 +1,36 @@
package db
import "github.com/gotunnel/pkg/protocol"
// Client 客户端数据
type Client struct {
ID string `json:"id"`
Rules []protocol.ProxyRule `json:"rules"`
}
// ClientStore 客户端存储接口
type ClientStore interface {
// GetAllClients 获取所有客户端
GetAllClients() ([]Client, error)
// GetClient 获取单个客户端
GetClient(id string) (*Client, error)
// CreateClient 创建客户端
CreateClient(c *Client) error
// UpdateClient 更新客户端
UpdateClient(c *Client) error
// DeleteClient 删除客户端
DeleteClient(id string) error
// ClientExists 检查客户端是否存在
ClientExists(id string) (bool, error)
// GetClientRules 获取客户端规则
GetClientRules(id string) ([]protocol.ProxyRule, error)
// Close 关闭连接
Close() error
}

View File

@@ -0,0 +1,146 @@
package db
import (
"database/sql"
"encoding/json"
"sync"
_ "github.com/mattn/go-sqlite3"
"github.com/gotunnel/pkg/protocol"
)
// SQLiteStore SQLite 存储实现
type SQLiteStore struct {
db *sql.DB
mu sync.RWMutex
}
// NewSQLiteStore 创建 SQLite 存储
func NewSQLiteStore(dbPath string) (*SQLiteStore, error) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, err
}
s := &SQLiteStore{db: db}
if err := s.init(); err != nil {
db.Close()
return nil, err
}
return s, nil
}
// init 初始化数据库表
func (s *SQLiteStore) init() error {
_, err := s.db.Exec(`
CREATE TABLE IF NOT EXISTS clients (
id TEXT PRIMARY KEY,
rules TEXT NOT NULL DEFAULT '[]'
);
`)
return err
}
// Close 关闭数据库连接
func (s *SQLiteStore) Close() error {
return s.db.Close()
}
// GetAllClients 获取所有客户端
func (s *SQLiteStore) GetAllClients() ([]Client, error) {
s.mu.RLock()
defer s.mu.RUnlock()
rows, err := s.db.Query(`SELECT id, rules FROM clients`)
if err != nil {
return nil, err
}
defer rows.Close()
var clients []Client
for rows.Next() {
var c Client
var rulesJSON string
if err := rows.Scan(&c.ID, &rulesJSON); err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(rulesJSON), &c.Rules); err != nil {
c.Rules = []protocol.ProxyRule{}
}
clients = append(clients, c)
}
return clients, nil
}
// GetClient 获取单个客户端
func (s *SQLiteStore) GetClient(id string) (*Client, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var c Client
var rulesJSON string
err := s.db.QueryRow(`SELECT id, rules FROM clients WHERE id = ?`, id).Scan(&c.ID, &rulesJSON)
if err != nil {
return nil, err
}
if err := json.Unmarshal([]byte(rulesJSON), &c.Rules); err != nil {
c.Rules = []protocol.ProxyRule{}
}
return &c, nil
}
// CreateClient 创建客户端
func (s *SQLiteStore) CreateClient(c *Client) error {
s.mu.Lock()
defer s.mu.Unlock()
rulesJSON, err := json.Marshal(c.Rules)
if err != nil {
return err
}
_, err = s.db.Exec(`INSERT INTO clients (id, rules) VALUES (?, ?)`, c.ID, string(rulesJSON))
return err
}
// UpdateClient 更新客户端
func (s *SQLiteStore) UpdateClient(c *Client) error {
s.mu.Lock()
defer s.mu.Unlock()
rulesJSON, err := json.Marshal(c.Rules)
if err != nil {
return err
}
_, err = s.db.Exec(`UPDATE clients SET rules = ? WHERE id = ?`, string(rulesJSON), c.ID)
return err
}
// DeleteClient 删除客户端
func (s *SQLiteStore) DeleteClient(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
_, err := s.db.Exec(`DELETE FROM clients WHERE id = ?`, id)
return err
}
// ClientExists 检查客户端是否存在
func (s *SQLiteStore) ClientExists(id string) (bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
var count int
err := s.db.QueryRow(`SELECT COUNT(*) FROM clients WHERE id = ?`, id).Scan(&count)
return count > 0, err
}
// GetClientRules 获取客户端规则
func (s *SQLiteStore) GetClientRules(id string) ([]protocol.ProxyRule, error) {
c, err := s.GetClient(id)
if err != nil {
return nil, err
}
return c.Rules, nil
}

View File

@@ -0,0 +1,216 @@
package router
import (
"encoding/json"
"net/http"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/pkg/protocol"
)
// ClientStatus 客户端状态
type ClientStatus struct {
ID string `json:"id"`
Online bool `json:"online"`
LastPing string `json:"last_ping,omitempty"`
RuleCount int `json:"rule_count"`
}
// ServerInterface 服务端接口
type ServerInterface interface {
GetClientStatus(clientID string) (online bool, lastPing string)
GetAllClientStatus() map[string]struct {
Online bool
LastPing string
}
ReloadConfig() error
GetBindAddr() string
GetBindPort() int
}
// AppInterface 应用接口
type AppInterface interface {
GetClientStore() db.ClientStore
GetServer() ServerInterface
}
// APIHandler API处理器
type APIHandler struct {
clientStore db.ClientStore
server ServerInterface
}
// RegisterRoutes 注册所有 API 路由
func RegisterRoutes(r *Router, app AppInterface) {
h := &APIHandler{
clientStore: app.GetClientStore(),
server: app.GetServer(),
}
api := r.Group("/api")
api.HandleFunc("/status", h.handleStatus)
api.HandleFunc("/clients", h.handleClients)
api.HandleFunc("/client/", h.handleClient)
api.HandleFunc("/config/reload", h.handleReload)
}
func (h *APIHandler) handleStatus(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
clients, _ := h.clientStore.GetAllClients()
status := map[string]interface{}{
"server": map[string]interface{}{
"bind_addr": h.server.GetBindAddr(),
"bind_port": h.server.GetBindPort(),
},
"client_count": len(clients),
}
h.jsonResponse(rw, status)
}
func (h *APIHandler) handleClients(rw http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.getClients(rw)
case http.MethodPost:
h.addClient(rw, r)
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *APIHandler) getClients(rw http.ResponseWriter) {
clients, err := h.clientStore.GetAllClients()
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
statusMap := h.server.GetAllClientStatus()
var result []ClientStatus
for _, c := range clients {
cs := ClientStatus{ID: c.ID, RuleCount: len(c.Rules)}
if s, ok := statusMap[c.ID]; ok {
cs.Online = s.Online
cs.LastPing = s.LastPing
}
result = append(result, cs)
}
h.jsonResponse(rw, result)
}
func (h *APIHandler) addClient(rw http.ResponseWriter, r *http.Request) {
var req struct {
ID string `json:"id"`
Rules []protocol.ProxyRule `json:"rules"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
if req.ID == "" {
http.Error(rw, "client id required", http.StatusBadRequest)
return
}
exists, _ := h.clientStore.ClientExists(req.ID)
if exists {
http.Error(rw, "client already exists", http.StatusConflict)
return
}
client := &db.Client{ID: req.ID, Rules: req.Rules}
if err := h.clientStore.CreateClient(client); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
h.jsonResponse(rw, map[string]string{"status": "ok"})
}
func (h *APIHandler) handleClient(rw http.ResponseWriter, r *http.Request) {
clientID := r.URL.Path[len("/api/client/"):]
if clientID == "" {
http.Error(rw, "client id required", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodGet:
h.getClient(rw, clientID)
case http.MethodPut:
h.updateClient(rw, r, clientID)
case http.MethodDelete:
h.deleteClient(rw, clientID)
default:
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (h *APIHandler) getClient(rw http.ResponseWriter, clientID string) {
client, err := h.clientStore.GetClient(clientID)
if err != nil {
http.Error(rw, "client not found", http.StatusNotFound)
return
}
online, lastPing := h.server.GetClientStatus(clientID)
h.jsonResponse(rw, map[string]interface{}{
"id": client.ID, "rules": client.Rules,
"online": online, "last_ping": lastPing,
})
}
func (h *APIHandler) updateClient(rw http.ResponseWriter, r *http.Request, clientID string) {
var req struct {
Rules []protocol.ProxyRule `json:"rules"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
exists, _ := h.clientStore.ClientExists(clientID)
if !exists {
http.Error(rw, "client not found", http.StatusNotFound)
return
}
client := &db.Client{ID: clientID, Rules: req.Rules}
if err := h.clientStore.UpdateClient(client); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
h.jsonResponse(rw, map[string]string{"status": "ok"})
}
func (h *APIHandler) deleteClient(rw http.ResponseWriter, clientID string) {
exists, _ := h.clientStore.ClientExists(clientID)
if !exists {
http.Error(rw, "client not found", http.StatusNotFound)
return
}
if err := h.clientStore.DeleteClient(clientID); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
h.jsonResponse(rw, map[string]string{"status": "ok"})
}
func (h *APIHandler) handleReload(rw http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(rw, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if err := h.server.ReloadConfig(); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
h.jsonResponse(rw, map[string]string{"status": "ok"})
}
func (h *APIHandler) jsonResponse(rw http.ResponseWriter, data interface{}) {
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(data)
}

View File

@@ -0,0 +1,51 @@
package router
import (
"net/http"
)
// Router 路由管理器
type Router struct {
mux *http.ServeMux
}
// New 创建路由管理器
func New() *Router {
return &Router{
mux: http.NewServeMux(),
}
}
// Handle 注册路由处理器
func (r *Router) Handle(pattern string, handler http.Handler) {
r.mux.Handle(pattern, handler)
}
// HandleFunc 注册路由处理函数
func (r *Router) HandleFunc(pattern string, handler http.HandlerFunc) {
r.mux.HandleFunc(pattern, handler)
}
// Group 创建路由组
func (r *Router) Group(prefix string) *RouteGroup {
return &RouteGroup{
router: r,
prefix: prefix,
}
}
// RouteGroup 路由组
type RouteGroup struct {
router *Router
prefix string
}
// HandleFunc 注册路由组处理函数
func (g *RouteGroup) HandleFunc(pattern string, handler http.HandlerFunc) {
g.router.mux.HandleFunc(g.prefix+pattern, handler)
}
// Handler 返回 http.Handler
func (r *Router) Handler() http.Handler {
return r.mux
}

View File

@@ -0,0 +1,335 @@
package tunnel
import (
"fmt"
"log"
"net"
"sync"
"time"
"github.com/gotunnel/internal/server/db"
"github.com/gotunnel/pkg/protocol"
"github.com/gotunnel/pkg/relay"
"github.com/gotunnel/pkg/utils"
"github.com/hashicorp/yamux"
)
// Server 隧道服务端
type Server struct {
clientStore db.ClientStore
bindAddr string
bindPort int
token string
heartbeat int
hbTimeout int
portManager *utils.PortManager
clients map[string]*ClientSession
mu sync.RWMutex
}
// ClientSession 客户端会话
type ClientSession struct {
ID string
Session *yamux.Session
Rules []protocol.ProxyRule
Listeners map[int]net.Listener
LastPing time.Time
mu sync.Mutex
}
// NewServer 创建服务端
func NewServer(cs db.ClientStore, bindAddr string, bindPort int, token string, heartbeat, hbTimeout int) *Server {
return &Server{
clientStore: cs,
bindAddr: bindAddr,
bindPort: bindPort,
token: token,
heartbeat: heartbeat,
hbTimeout: hbTimeout,
portManager: utils.NewPortManager(),
clients: make(map[string]*ClientSession),
}
}
// Run 启动服务端
func (s *Server) Run() error {
addr := fmt.Sprintf("%s:%d", s.bindAddr, s.bindPort)
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("failed to listen on %s: %v", addr, err)
}
defer ln.Close()
log.Printf("[Server] Listening on %s", addr)
for {
conn, err := ln.Accept()
if err != nil {
log.Printf("[Server] Accept error: %v", err)
continue
}
go s.handleConnection(conn)
}
}
// handleConnection 处理客户端连接
func (s *Server) handleConnection(conn net.Conn) {
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
msg, err := protocol.ReadMessage(conn)
if err != nil {
log.Printf("[Server] Read auth error: %v", err)
return
}
if msg.Type != protocol.MsgTypeAuth {
log.Printf("[Server] Expected auth, got %d", msg.Type)
return
}
var authReq protocol.AuthRequest
if err := msg.ParsePayload(&authReq); err != nil {
log.Printf("[Server] Parse auth error: %v", err)
return
}
if authReq.Token != s.token {
s.sendAuthResponse(conn, false, "invalid token")
return
}
rules, err := s.clientStore.GetClientRules(authReq.ClientID)
if err != nil || rules == nil {
s.sendAuthResponse(conn, false, "client not configured")
return
}
conn.SetReadDeadline(time.Time{})
if err := s.sendAuthResponse(conn, true, "ok"); err != nil {
return
}
log.Printf("[Server] Client %s authenticated", authReq.ClientID)
s.setupClientSession(conn, authReq.ClientID, rules)
}
// setupClientSession 建立客户端会话
func (s *Server) setupClientSession(conn net.Conn, clientID string, rules []protocol.ProxyRule) {
session, err := yamux.Server(conn, nil)
if err != nil {
log.Printf("[Server] Yamux error: %v", err)
return
}
cs := &ClientSession{
ID: clientID,
Session: session,
Rules: rules,
Listeners: make(map[int]net.Listener),
LastPing: time.Now(),
}
s.registerClient(cs)
defer s.unregisterClient(cs)
if err := s.sendProxyConfig(session, rules); err != nil {
log.Printf("[Server] Send config error: %v", err)
return
}
s.startProxyListeners(cs)
go s.heartbeatLoop(cs)
<-session.CloseChan()
log.Printf("[Server] Client %s disconnected", clientID)
}
// sendAuthResponse 发送认证响应
func (s *Server) sendAuthResponse(conn net.Conn, success bool, message string) error {
resp := protocol.AuthResponse{Success: success, Message: message}
msg, _ := protocol.NewMessage(protocol.MsgTypeAuthResp, resp)
return protocol.WriteMessage(conn, msg)
}
// sendProxyConfig 发送代理配置
func (s *Server) sendProxyConfig(session *yamux.Session, rules []protocol.ProxyRule) error {
stream, err := session.Open()
if err != nil {
return err
}
defer stream.Close()
cfg := protocol.ProxyConfig{Rules: rules}
msg, _ := protocol.NewMessage(protocol.MsgTypeProxyConfig, cfg)
return protocol.WriteMessage(stream, msg)
}
// registerClient 注册客户端
func (s *Server) registerClient(cs *ClientSession) {
s.mu.Lock()
defer s.mu.Unlock()
s.clients[cs.ID] = cs
}
// unregisterClient 注销客户端
func (s *Server) unregisterClient(cs *ClientSession) {
s.mu.Lock()
defer s.mu.Unlock()
cs.mu.Lock()
for port, ln := range cs.Listeners {
ln.Close()
s.portManager.Release(port)
}
cs.mu.Unlock()
delete(s.clients, cs.ID)
}
// startProxyListeners 启动代理监听
func (s *Server) startProxyListeners(cs *ClientSession) {
for _, rule := range cs.Rules {
if err := s.portManager.Reserve(rule.RemotePort, cs.ID); err != nil {
log.Printf("[Server] Port %d error: %v", rule.RemotePort, err)
continue
}
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", rule.RemotePort))
if err != nil {
log.Printf("[Server] Listen %d error: %v", rule.RemotePort, err)
s.portManager.Release(rule.RemotePort)
continue
}
cs.mu.Lock()
cs.Listeners[rule.RemotePort] = ln
cs.mu.Unlock()
log.Printf("[Server] Proxy %s: :%d -> %s:%d",
rule.Name, rule.RemotePort, rule.LocalIP, rule.LocalPort)
go s.acceptProxyConns(cs, ln, rule)
}
}
// acceptProxyConns 接受代理连接
func (s *Server) acceptProxyConns(cs *ClientSession, ln net.Listener, rule protocol.ProxyRule) {
for {
conn, err := ln.Accept()
if err != nil {
return
}
go s.handleProxyConn(cs, conn, rule)
}
}
// handleProxyConn 处理代理连接
func (s *Server) handleProxyConn(cs *ClientSession, conn net.Conn, rule protocol.ProxyRule) {
defer conn.Close()
stream, err := cs.Session.Open()
if err != nil {
log.Printf("[Server] Open stream error: %v", err)
return
}
defer stream.Close()
req := protocol.NewProxyRequest{RemotePort: rule.RemotePort}
msg, _ := protocol.NewMessage(protocol.MsgTypeNewProxy, req)
if err := protocol.WriteMessage(stream, msg); err != nil {
return
}
relay.Relay(conn, stream)
}
// heartbeatLoop 心跳检测循环
func (s *Server) heartbeatLoop(cs *ClientSession) {
ticker := time.NewTicker(time.Duration(s.heartbeat) * time.Second)
defer ticker.Stop()
timeout := time.Duration(s.hbTimeout) * time.Second
for {
select {
case <-ticker.C:
cs.mu.Lock()
if time.Since(cs.LastPing) > timeout {
cs.mu.Unlock()
log.Printf("[Server] Client %s heartbeat timeout", cs.ID)
cs.Session.Close()
return
}
cs.mu.Unlock()
stream, err := cs.Session.Open()
if err != nil {
return
}
msg := &protocol.Message{Type: protocol.MsgTypeHeartbeat}
protocol.WriteMessage(stream, msg)
stream.Close()
case <-cs.Session.CloseChan():
return
}
}
}
// GetClientStatus 获取客户端状态
func (s *Server) GetClientStatus(clientID string) (online bool, lastPing string) {
s.mu.RLock()
defer s.mu.RUnlock()
if cs, ok := s.clients[clientID]; ok {
cs.mu.Lock()
defer cs.mu.Unlock()
return true, cs.LastPing.Format(time.RFC3339)
}
return false, ""
}
// GetAllClientStatus 获取所有客户端状态
func (s *Server) GetAllClientStatus() map[string]struct {
Online bool
LastPing string
} {
s.mu.RLock()
defer s.mu.RUnlock()
result := make(map[string]struct {
Online bool
LastPing string
})
for id, cs := range s.clients {
cs.mu.Lock()
result[id] = struct {
Online bool
LastPing string
}{
Online: true,
LastPing: cs.LastPing.Format(time.RFC3339),
}
cs.mu.Unlock()
}
return result
}
// ReloadConfig 重新加载配置
func (s *Server) ReloadConfig() error {
return nil
}
// GetBindAddr 获取绑定地址
func (s *Server) GetBindAddr() string {
return s.bindAddr
}
// GetBindPort 获取绑定端口
func (s *Server) GetBindPort() int {
return s.bindPort
}