PortRelay:把内网服务临时、可控地开放给同事
边缘网关 + Agent 的端口中继:Agent 主动连出到网关,把内网目标挂到网关端口上; 通过邀请链接按设备授权,可选审批,首次连接起计时,到期、闲置自动收回。 - portrelay-server:边缘网关(端口池、白名单与授权租约、邀请与授权网页) - portrelay-agent:Agent(yamux 隧道、本地 API 与网页面板、流量统计) - mac-native:SwiftUI 菜单栏 App
This commit is contained in:
commit
86e159b3f0
67 files changed
+13888
No files matched your search
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
|
||||
"github.com/DeepFal/PortRelay/client"
|
||||
)
|
||||
|
||||
//go:embed all:web
|
||||
var webFS embed.FS
|
||||
|
||||
// version 由发布构建通过 -ldflags "-X main.version=..." 注入
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
httpPort := flag.Int("port", 33080, "Local dashboard HTTP port")
|
||||
noBrowser := flag.Bool("no-browser", false, "Do not auto-open browser")
|
||||
showVersion := flag.Bool("version", false, "Print version and exit")
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Println("portrelay-agent", version)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("==================================================")
|
||||
log.Printf(" PortRelay Agent %s", version)
|
||||
log.Printf(" Dashboard: http://127.0.0.1:%d", *httpPort)
|
||||
log.Printf("==================================================")
|
||||
|
||||
// 先占住面板端口:已有实例在运行时直接退出,避免第二个 Agent 连上服务端顶替会话
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", *httpPort)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("Dashboard port %s unavailable (is another PortRelay agent running?): %v", addr, err)
|
||||
}
|
||||
|
||||
cm, err := client.NewConfigManager()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to init config manager: %v", err)
|
||||
}
|
||||
|
||||
agent := client.NewAgent(cm)
|
||||
agent.Start()
|
||||
|
||||
// Strip 'web' prefix for embed fs
|
||||
subFS, err := fs.Sub(webFS, "web")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load embedded web assets: %v", err)
|
||||
}
|
||||
|
||||
apiServer := client.NewAPIServer(cm, agent, http.FS(subFS))
|
||||
apiServer.OnShutdown = func() {
|
||||
log.Printf("Shutdown requested via API, stopping PortRelay Agent...")
|
||||
agent.Stop()
|
||||
_ = cm.Close()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigChan
|
||||
log.Printf("\nShutting down PortRelay Agent...")
|
||||
agent.Stop()
|
||||
_ = cm.Close()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
if !*noBrowser {
|
||||
go openBrowser(fmt.Sprintf("http://127.0.0.1:%d", *httpPort))
|
||||
}
|
||||
|
||||
log.Printf("Serving local web control panel at http://%s", addr)
|
||||
if err := http.Serve(ln, apiServer.Handler()); err != nil {
|
||||
agent.Stop()
|
||||
_ = cm.Close()
|
||||
log.Fatalf("HTTP server failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func openBrowser(url string) {
|
||||
var cmd *exec.Cmd
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
cmd = exec.Command("open", url)
|
||||
case "windows":
|
||||
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
|
||||
default:
|
||||
cmd = exec.Command("xdg-open", url)
|
||||
}
|
||||
_ = cmd.Start()
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/DeepFal/PortRelay/server"
|
||||
)
|
||||
|
||||
type peekedConn struct {
|
||||
net.Conn
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (c *peekedConn) Read(b []byte) (int, error) {
|
||||
return c.r.Read(b)
|
||||
}
|
||||
|
||||
// version 由发布构建通过 -ldflags "-X main.version=..." 注入
|
||||
var version = "dev"
|
||||
|
||||
func envOr(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func main() {
|
||||
listenAddr := flag.String("listen", ":33000", "Control/relay listen address")
|
||||
portMin := flag.Int("port-min", 33000, "Minimum tunnel port")
|
||||
portMax := flag.Int("port-max", 34000, "Maximum tunnel port")
|
||||
token := flag.String("token", envOr("PORTRELAY_TOKEN", ""), "Shared secret the agent must present (or env PORTRELAY_TOKEN); required")
|
||||
publicHost := flag.String("public-host", "", "Host shown on the web auth page (default: the host the visitor used to open the link)")
|
||||
stateFile := flag.String("state", "/var/lib/portrelay/leases.json", "File that keeps temporary (web auth) leases across restarts; empty disables")
|
||||
trustProxy := flag.Bool("trust-proxy", false, "Trust X-Forwarded-For/X-Real-IP on /auth (only behind a trusted reverse proxy)")
|
||||
showVersion := flag.Bool("version", false, "Print version and exit")
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Println("portrelay-server", version)
|
||||
return
|
||||
}
|
||||
|
||||
if *token == "" {
|
||||
log.Fatalf("Fatal: no token configured; set -token or PORTRELAY_TOKEN (e.g. openssl rand -hex 24)")
|
||||
}
|
||||
if len(*token) < 16 {
|
||||
log.Printf("WARNING: token is shorter than 16 characters; use a long random value")
|
||||
}
|
||||
|
||||
log.Printf("==================================================")
|
||||
log.Printf(" PortRelay Relay Server %s", version)
|
||||
log.Printf(" Control/Web Port: %s", *listenAddr)
|
||||
log.Printf(" Allowed Range: %d - %d", *portMin, *portMax)
|
||||
log.Printf("==================================================")
|
||||
|
||||
mgr := server.NewManager(*portMin, *portMax, *token)
|
||||
mgr.PublicHost = *publicHost
|
||||
mgr.TrustProxyHeaders = *trustProxy
|
||||
if *stateFile != "" {
|
||||
if err := mgr.EnableLeasePersistence(*stateFile); err != nil {
|
||||
log.Printf("WARNING: lease persistence disabled: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", *listenAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("Fatal: failed to listen on %s: %v", *listenAddr, err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
// Graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-sigChan
|
||||
log.Printf("\nShutting down PortRelay Server...")
|
||||
_ = ln.Close()
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-sigChan:
|
||||
return
|
||||
default:
|
||||
log.Printf("Accept error: %v", err)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
go func(c net.Conn) {
|
||||
// 不发数据的空连接最多占用 10 秒
|
||||
_ = c.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
br := bufio.NewReader(c)
|
||||
peek, err := br.Peek(3)
|
||||
if err != nil {
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
_ = c.SetReadDeadline(time.Time{})
|
||||
if string(peek) == "GET" || string(peek) == "POS" || string(peek) == "HEA" {
|
||||
mgr.HandleHTTP(&peekedConn{Conn: c, r: br})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Incoming relay agent connection from %s", c.RemoteAddr())
|
||||
if err := mgr.HandleSession(&peekedConn{Conn: c, r: br}); err != nil {
|
||||
log.Printf("Session closed for %s: %v", c.RemoteAddr(), err)
|
||||
}
|
||||
}(conn)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user