v2
lzf 2024-09-30 16:37:53 +08:00
parent 1b27f3b2ab
commit 8239477e53
34 changed files with 3925 additions and 142 deletions

3
.gitignore vendored
View File

@ -3,4 +3,5 @@ bin
go.sum go.sum
config.toml config.toml
temp temp
logs logs
*.db

42
app/db/db.go Normal file
View File

@ -0,0 +1,42 @@
package db
import (
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
var (
db *gorm.DB
)
func InitDB() {
gdb, err := gorm.Open(sqlite.Open("data.db"))
if err != nil {
panic(err)
}
db = gdb.Debug()
err = db.Exec(createTable).Error
if err != nil {
panic(err)
}
}
func DB() *gorm.DB {
return db
}
var (
createTable = `
create table if not exists forward (
id integer primary key not null,
name varchar(60) not null,
local_ip text not null,
local_port integer not null,
target_addr text not null,
protocol integer default 0 not null,
status integer default 0 not null,
create_time datetime DEFAULT CURRENT_TIMESTAMP not null,
update_time datetime
)`
)

201
app/forward/forward.go Normal file
View File

@ -0,0 +1,201 @@
package forward
import (
"context"
"fmt"
"git.makemake.in/kzkzzzz/mycommon/mylog"
"proxyport/app/db"
"proxyport/app/model"
"sync"
)
type Protocol int
func (p Protocol) String() string {
switch p {
case 0:
return "TCP"
case 1:
return "UDP"
}
return "unknown"
}
const (
ProtocolTCP Protocol = 0
ProtocolUDP Protocol = 1
)
type IForward interface {
Forward() error
Stop()
}
var ListenerManager = &Manager{
lock: &sync.Mutex{},
}
type Info struct {
Id int
Name string
TargetAddr []string
LocalAddr string
LocalIp string
LocalPort int
Protocol Protocol
Status int
}
type activeForward struct {
forward IForward
info *Info
}
type forwardKey struct {
localAddr string
protocol Protocol
}
type Manager struct {
activeForwardMap map[forwardKey]*activeForward
lock *sync.Mutex
}
func (m *Manager) initForwardList() []*Info {
data := make([]*model.Forward, 0)
err := db.DB().Table("forward").Select("*").Find(&data).Error
if err != nil {
panic(err)
}
list := make([]*Info, 0)
for _, v := range data {
if v.Status == 0 {
continue
}
list = append(list, m.convertModel(v))
}
return list
}
func (m *Manager) convertModel(v *model.Forward) *Info {
info := &Info{
Id: v.Id,
Name: v.Name,
TargetAddr: v.TargetAddr,
LocalIp: v.LocalIp,
LocalPort: v.LocalPort,
Protocol: Protocol(v.Protocol),
Status: v.Status,
}
if v.LocalIp == "" {
info.LocalAddr = fmt.Sprintf("0.0.0.0:%d", v.LocalPort)
} else {
info.LocalAddr = fmt.Sprintf("%s:%d", v.LocalIp, v.LocalPort)
}
return info
}
func (m *Manager) Add(v *model.Forward) error {
m.lock.Lock()
defer m.lock.Unlock()
forwardInfo := m.convertModel(v)
key := forwardKey{
localAddr: forwardInfo.LocalAddr,
protocol: forwardInfo.Protocol,
}
_, ok := m.activeForwardMap[key]
if ok {
return fmt.Errorf("[%s] [%s] is exist", forwardInfo.Protocol, forwardInfo.LocalAddr)
}
tmp := &activeForward{
info: forwardInfo,
}
if Protocol(v.Protocol) == ProtocolUDP {
tmp.forward = NewUDP(forwardInfo)
} else {
tmp.forward = NewTCP(forwardInfo)
}
m.activeForwardMap[key] = tmp
err := tmp.forward.Forward()
if err != nil {
delete(m.activeForwardMap, key)
return err
}
return nil
}
func (m *Manager) Remove(v *model.Forward) {
m.lock.Lock()
defer m.lock.Unlock()
forwardInfo := m.convertModel(v)
key := forwardKey{
localAddr: forwardInfo.LocalAddr,
protocol: forwardInfo.Protocol,
}
fr, ok := m.activeForwardMap[key]
if !ok {
return
}
fr.forward.Stop()
delete(m.activeForwardMap, key)
}
func (m *Manager) Start(ctx context.Context) {
m.activeForwardMap = make(map[forwardKey]*activeForward)
list := m.initForwardList()
for _, v := range list {
key := forwardKey{
localAddr: v.LocalAddr,
protocol: v.Protocol,
}
switch v.Protocol {
case ProtocolTCP:
m.activeForwardMap[key] = &activeForward{
forward: NewTCP(v),
info: v,
}
case ProtocolUDP:
m.activeForwardMap[key] = &activeForward{
forward: NewUDP(v),
info: v,
}
}
}
for _, v := range m.activeForwardMap {
err := v.forward.Forward()
if err != nil {
mylog.Error(err)
}
}
select {
case <-ctx.Done():
m.Stop()
}
}
func (m *Manager) Stop() {
for _, v := range m.activeForwardMap {
v.forward.Stop()
}
}

135
app/forward/tcp.go Normal file
View File

@ -0,0 +1,135 @@
package forward
import (
"context"
"fmt"
"git.makemake.in/kzkzzzz/mycommon/mylog"
"io"
"math/rand"
"net"
"time"
)
var _ IForward = (*TCP)(nil)
type TCP struct {
forwardInfo *Info
listener net.Listener
}
func NewTCP(forwardInfo *Info) *TCP {
return &TCP{
forwardInfo: forwardInfo,
}
}
func (t *TCP) Forward() error {
listenTimeout := time.Second
ctx0, cancel0 := context.WithTimeout(context.Background(), listenTimeout)
defer cancel0()
var (
errChan = make(chan error, 1)
listenerRes = make(chan net.Listener, 1)
)
go func() {
listener, err := net.Listen("tcp", t.forwardInfo.LocalAddr)
if err != nil {
mylog.Error(err)
errChan <- err
return
}
listenerRes <- listener
}()
select {
case listener := <-listenerRes:
t.listener = listener
case err := <-errChan:
return err
case <-ctx0.Done():
return fmt.Errorf("listen timeout %s %s", t.forwardInfo.LocalAddr, listenTimeout)
}
//
//mylog.Infof("start listen: %s", t.forwardInfo.LocalAddr)
//lc := net.ListenConfig{}
//listener, err := lc.Listen(ctx0, "tcp", t.forwardInfo.LocalAddr)
// 启动 TCP 监听
mylog.Infof("[TCP] %s %s -> %s", t.forwardInfo.Name, t.forwardInfo.LocalAddr, t.forwardInfo.TargetAddr)
go func() {
ctx, cancelCtx := context.WithCancel(context.Background())
defer cancelCtx()
for {
// 接受连接
conn, err := t.listener.Accept()
if err != nil {
mylog.Error(err)
break
}
// 处理连接
go t.handleConn(ctx, conn, t.forwardInfo.TargetAddr)
}
}()
return nil
}
func (t *TCP) handleConn(mainCtx context.Context, localConn net.Conn, targetAddrList []string) {
defer localConn.Close()
targetAddr := targetAddrList[rand.Intn(len(targetAddrList))]
// 连接到目标地址
targetConn, err := net.Dial("tcp", targetAddr)
if err != nil {
mylog.Error("Error connecting to target:", err)
return
}
defer targetConn.Close()
ctx, cancelCtx := context.WithCancel(context.Background())
defer cancelCtx()
defer func() {
mylog.Warnf("tcp forward stop %s -> %+v", localConn.RemoteAddr(), targetAddrList)
}()
mylog.Debugf("tcp forward %s -> %s", localConn.RemoteAddr(), targetAddr)
go func() {
defer cancelCtx()
_, err := io.Copy(targetConn, localConn) // 从客户端转发到目标
if err != nil {
mylog.Error(err)
}
}()
go func() {
defer cancelCtx()
_, err := io.Copy(localConn, targetConn) // 从目标转发到客户端
if err != nil {
mylog.Error(err)
}
}()
select {
case <-mainCtx.Done():
cancelCtx()
case <-ctx.Done():
}
}
func (t *TCP) Stop() {
if t.listener != nil {
t.listener.Close()
}
}

123
app/forward/udp.go Normal file
View File

@ -0,0 +1,123 @@
package forward
import (
"git.makemake.in/kzkzzzz/mycommon/mylog"
"math/rand"
"net"
"time"
)
var _ IForward = (*UDP)(nil)
type UDP struct {
forwardInfo *Info
conn *net.UDPConn
packetBufferSize int
messageTimeout time.Duration
}
func NewUDP(forwardInfo *Info) *UDP {
return &UDP{
forwardInfo: forwardInfo,
packetBufferSize: 2048,
messageTimeout: time.Second * 2,
}
}
func (u *UDP) Forward() error {
udpLocalAddr, err := net.ResolveUDPAddr("udp", u.forwardInfo.LocalAddr)
if err != nil {
return err
}
conn, err := net.ListenUDP("udp", udpLocalAddr)
if err != nil {
return err
}
u.conn = conn
mylog.Infof("[UDP] %s %s -> %s", u.forwardInfo.Name, u.forwardInfo.LocalAddr, u.forwardInfo.TargetAddr)
go func() {
err := u.handleConn(conn)
if err != nil {
mylog.Error(err)
}
}()
return nil
}
func (u *UDP) handleConn(conn *net.UDPConn) error {
defer func() {
mylog.Warnf("udp forward stop %s -> %+v", u.forwardInfo.LocalAddr, u.forwardInfo.TargetAddr)
}()
buffer := make([]byte, u.packetBufferSize) // 创建一个缓冲区
for {
// 接收数据
n, clientAddr, err := conn.ReadFromUDP(buffer)
if err != nil {
mylog.Error(err)
return err
}
targetAddr := u.forwardInfo.TargetAddr[rand.Intn(len(u.forwardInfo.TargetAddr))]
mylog.Debugf("udp forward %s -> %s", clientAddr.String(), targetAddr)
// 解析目标地址
//remoteAddr, err := net.ResolveUDPAddr("udp", targetAddr)
//if err != nil {
// mylog.Error(err)
// return err
//}
dialer := &net.Dialer{
Timeout: u.messageTimeout,
}
remoteRawConn, err := dialer.Dial("udp", targetAddr)
if err != nil {
mylog.Error(err)
continue
}
remoteRawConn.SetWriteDeadline(time.Now().Add(u.messageTimeout))
remoteRawConn.SetReadDeadline(time.Now().Add(u.messageTimeout))
remoteConn := remoteRawConn.(*net.UDPConn)
//remoteConn, err := net.DialUDP("udp", nil, remoteAddr)
//dial, err := reuseport.Dial("udp", "", targetAddr)
// 将数据转发到目标地址
_, err = remoteConn.Write(buffer[:n])
if err != nil {
mylog.Error(err)
continue
}
res := make([]byte, u.packetBufferSize)
resLen, _, err := remoteConn.ReadFromUDP(res)
if err != nil {
mylog.Error(err)
continue
}
// 将目标地址的响应返回给客户端
_, err = conn.WriteToUDP(res[:resLen], clientAddr)
if err != nil {
mylog.Error(err)
continue
}
}
}
func (u *UDP) Stop() {
if u.conn != nil {
u.conn.Close()
}
}

38
app/model/forward.go Normal file
View File

@ -0,0 +1,38 @@
package model
import (
"database/sql/driver"
"fmt"
"strings"
)
type Forward struct {
Id int `json:"id"`
Name string `json:"name"`
LocalIp string `json:"local_ip"`
LocalPort int `json:"local_port"`
TargetAddr StringList `json:"target_addr"`
Protocol int `json:"protocol"`
Status int `json:"status"`
CreateTime string `json:"create_time"`
UpdateTime string `json:"update_time"`
}
type StringList []string
func (s *StringList) Scan(value interface{}) error {
v, ok := value.(string)
if !ok {
return fmt.Errorf("value is not string")
}
*s = strings.Split(v, ",")
return nil
}
func (s StringList) Value() (driver.Value, error) {
if len(s) == 0 {
return "", nil
}
return strings.Join(s, ","), nil
}

View File

@ -1,126 +0,0 @@
package app
import (
"context"
"git.makemake.in/kzkzzzz/mycommon/myconf"
"git.makemake.in/kzkzzzz/mycommon/mylog"
"io"
"math/rand"
"net"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
type ProxyItem struct {
RemoteAddr []string
LocalAddr string
}
var (
mainWg = &sync.WaitGroup{}
mainCtx, mainCancel = context.WithCancel(context.Background())
proxyInfo map[string]*ProxyItem
)
func Run() {
err := myconf.Conf().UnmarshalKey("proxy", &proxyInfo)
if err != nil {
mylog.Error(err)
return
}
names := make([]string, 0, len(proxyInfo))
for name := range proxyInfo {
names = append(names, name)
}
for _, name := range names {
go listenTcp(name, proxyInfo[name])
}
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
v := <-ch
mylog.Infof("捕获退出信号: %s", v.String())
mainCancel()
mainWg.Wait()
mylog.Info("stop")
}
func listenTcp(name string, item *ProxyItem) {
listen, err := net.Listen("tcp4", item.LocalAddr)
if err != nil {
mylog.Errorf("[%s] listen err: %s", name, err)
return
}
mylog.Infof("[%s] forward %s -> %+v", name, item.LocalAddr, item.RemoteAddr)
for {
localConn, err := listen.Accept()
if err != nil {
mylog.Errorf("conn accept err: %s", item.LocalAddr, err)
return
}
mainWg.Add(1)
go proxyTcp(localConn, item)
}
}
func proxyTcp(localConn net.Conn, item *ProxyItem) {
defer mainWg.Done()
randRemoteAddr := item.RemoteAddr[rand.Intn(len(item.RemoteAddr))]
remoteConn, err := net.DialTimeout("tcp", randRemoteAddr, time.Second*3)
if err != nil {
localConn.Close()
mylog.Errorf("connect remote err: %s", err)
return
}
mylog.Debugf("start proxy %s -> %s", localConn.RemoteAddr(), randRemoteAddr)
waitCh := make(chan struct{})
go func() {
wg0 := &sync.WaitGroup{}
wg0.Add(2)
go copyConn(wg0, localConn, remoteConn)
go copyConn(wg0, remoteConn, localConn)
wg0.Wait()
close(waitCh)
}()
select {
case <-waitCh:
case <-mainCtx.Done():
localConn.Close()
remoteConn.Close()
}
mylog.Debugf("stop proxy %s -> %s", localConn.RemoteAddr(), randRemoteAddr)
}
func copyConn(wg0 *sync.WaitGroup, localConn, remoteConn net.Conn) {
defer func() {
wg0.Done()
localConn.Close()
remoteConn.Close()
}()
_, err := io.Copy(remoteConn, localConn)
if err != nil {
mylog.Debug(err)
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,42 @@
package static
import (
"embed"
"github.com/gin-gonic/gin"
"html/template"
"io/fs"
"net/http"
"strings"
)
var (
//go:embed assets
assetsFs embed.FS
//go:embed view
viewFs embed.FS
)
func LoadStatic(engine *gin.Engine) {
sub, err := fs.Sub(assetsFs, "assets")
if err != nil {
panic(err)
}
cacheSuffix := []string{".css", ".js", ".js.map", ".css.map"}
engine.Use(func(ctx *gin.Context) {
if ctx.Request.Method == http.MethodGet {
for _, suffix := range cacheSuffix {
if strings.HasSuffix(ctx.Request.URL.Path, suffix) {
ctx.Writer.Header().Set("Cache-Control", "max-age=7200")
break
}
}
}
ctx.Next()
})
engine.StaticFS("/assets", http.FS(sub))
engine.SetHTMLTemplate(template.Must(template.New("").ParseFS(viewFs, "view/*")))
}

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>端口转发配置</title>
<script type="module" crossorigin src="./assets/index-lqpwyav5.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BA8XwqWY.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

464
app/web/web.go Normal file
View File

@ -0,0 +1,464 @@
package web
import (
"context"
"fmt"
"git.makemake.in/kzkzzzz/mycommon/mylog"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTr "github.com/go-playground/validator/v10/translations/en"
"github.com/spf13/cast"
"gorm.io/gorm"
"net/http"
"proxyport/app/db"
"proxyport/app/forward"
"proxyport/app/model"
"proxyport/app/static"
"reflect"
"regexp"
"strings"
"time"
)
var Config = &Cfg{}
type Cfg struct {
ListenAddr string
User string
Password string
}
func Start(ctx context.Context) {
InitGinTrans()
engine := gin.Default()
if Config.User != "" && Config.Password != "" {
engine.Use(gin.BasicAuth(gin.Accounts{
Config.User: Config.Password,
}))
}
static.LoadStatic(engine)
engine.Use(cors())
engine.GET("/", func(ctx *gin.Context) {
ctx.HTML(http.StatusOK, "index.html", nil)
})
engine.GET("/List", List)
engine.POST("/Create", Create)
engine.POST("/Update", Update)
engine.POST("/Delete", Delete)
engine.POST("/SwitchStatus", SwitchStatus)
hs := &http.Server{
Addr: Config.ListenAddr,
Handler: engine,
}
go func() {
mylog.Infof("http listen on %s", hs.Addr)
mylog.Warn(hs.ListenAndServe())
}()
<-ctx.Done()
hs.Close()
}
func cors() gin.HandlerFunc {
return func(ctx *gin.Context) {
ctx.Header("Access-Control-Allow-Origin", "*")
ctx.Header("Access-Control-Allow-Methods", "GET, POST")
ctx.Header("Access-Control-Allow-Headers", "Content-Type")
if ctx.Request.Method == http.MethodOptions {
ctx.AbortWithStatus(http.StatusOK)
} else {
ctx.Next()
}
}
}
func List(ctx *gin.Context) {
data := make([]*model.Forward, 0)
err := db.DB().Table("forward").Select("*").Order("update_time desc").Find(&data).Error
if err != nil {
fail(ctx, err)
return
}
success(ctx, data)
}
func Create(ctx *gin.Context) {
type Req struct {
TargetAddr string `json:"target_addr" binding:"min=1"`
LocalPort int `json:"local_port" binding:"required"`
Name string `json:"name" binding:"required"`
ForwardType int `json:"protocol" binding:"oneof=0 1"`
Status int `json:"status" binding:"oneof=0 1"`
}
req := &Req{}
err := ctx.ShouldBindJSON(req)
if err != nil {
fail(ctx, err)
return
}
err = checkAddr(fmt.Sprintf("0.0.0.0:%d", req.LocalPort))
if err != nil {
fail(ctx, err)
return
}
targetAddr, err := parseAddrList(req.TargetAddr)
if err != nil {
fail(ctx, err)
return
}
err = checkUnique(0, req.LocalPort, req.ForwardType)
if err != nil {
fail(ctx, err)
return
}
now := time.Now()
err = db.DB().Transaction(func(tx *gorm.DB) error {
mForward := &model.Forward{
LocalPort: req.LocalPort,
TargetAddr: targetAddr,
Name: req.Name,
Protocol: req.ForwardType,
Status: 1,
CreateTime: now.Format(time.DateTime),
UpdateTime: now.Format(time.DateTime),
}
err := tx.Table("forward").Create(mForward).Error
if err != nil {
return err
}
err = forward.ListenerManager.Add(mForward)
if err != nil {
return err
}
return nil
})
if err != nil {
fail(ctx, err)
return
}
success(ctx, "ok")
}
func Update(ctx *gin.Context) {
type Req struct {
Id int `json:"id" binding:"required"`
TargetAddr string `json:"target_addr" binding:"min=1"`
LocalPort int `json:"local_port" binding:"required"`
Name string `json:"name" binding:"required"`
Protocol int `json:"protocol" binding:"oneof=0 1"`
Status int `json:"status" binding:"oneof=0 1"`
}
req := &Req{}
err := ctx.ShouldBindJSON(req)
if err != nil {
fail(ctx, err)
return
}
err = checkAddr(fmt.Sprintf("0.0.0.0:%d", req.LocalPort))
if err != nil {
fail(ctx, err)
return
}
targetAddr, err := parseAddrList(req.TargetAddr)
if err != nil {
fail(ctx, err)
return
}
err = checkUnique(req.Id, req.LocalPort, req.Protocol)
if err != nil {
fail(ctx, err)
return
}
current, err := getForwardById(req.Id)
if err != nil {
fail(ctx, err)
return
}
err = db.DB().Transaction(func(tx *gorm.DB) error {
mForward := &model.Forward{
LocalPort: req.LocalPort,
TargetAddr: targetAddr,
Name: req.Name,
Protocol: req.Protocol,
UpdateTime: time.Now().Format(time.DateTime),
}
err = tx.Table("forward").Select("*").Omit("create_time", "status").Where("id = ?", req.Id).
Limit(1).
Updates(mForward).Error
if err != nil {
return err
}
forward.ListenerManager.Remove(current)
if current.Status == 1 {
err = forward.ListenerManager.Add(mForward)
if err != nil {
return err
}
}
return nil
})
if err != nil {
fail(ctx, err)
return
}
success(ctx, "ok")
}
func Delete(ctx *gin.Context) {
type Req struct {
Id int `json:"id" binding:"required"`
}
req := &Req{}
err := ctx.ShouldBindJSON(req)
if err != nil {
fail(ctx, err)
return
}
mForward, err := getForwardById(req.Id)
if err != nil {
fail(ctx, err)
return
}
err = db.DB().Table("forward").Select("*").Where("id = ?", req.Id).Limit(1).Delete(nil).Error
if err != nil {
fail(ctx, err)
return
}
forward.ListenerManager.Remove(mForward)
success(ctx, "ok")
}
var addrReg = regexp.MustCompile(`^[^:]+:([0-9]{1,5})$`)
func checkAddr(addr string) error {
res := addrReg.FindStringSubmatch(addr)
if len(res) != 2 {
return fmt.Errorf("invalid addr: %s", addr)
}
intPort := cast.ToInt(res[1])
if intPort < 10 || intPort > 65535 {
return fmt.Errorf("port: %d out of range: 10 - 65535", intPort)
}
//_, err := netip.ParseAddrPort(addr)
//if err != nil {
// return err
//}
return nil
}
func checkUnique(id, localPort, forwardType int) error {
query := db.DB().Table("forward").
Where("local_port = ?", localPort).
Where("protocol = ?", forwardType)
if id > 0 {
query.Where("id != ?", id)
}
var res []int
err := query.Select("id").Limit(1).Find(&res).Error
if err != nil {
return err
}
if len(res) > 0 {
return fmt.Errorf("%s port: %d already use", forward.Protocol(forwardType).String(), localPort)
}
return nil
}
func getForwardById(id int) (*model.Forward, error) {
if id <= 0 {
return nil, fmt.Errorf("id err: %d", id)
}
mForward := &model.Forward{}
err := db.DB().Table("forward").Select("*").Where("id = ?", id).First(mForward).Error
if err != nil {
return nil, err
}
return mForward, nil
}
func SwitchStatus(ctx *gin.Context) {
type Req struct {
Id int `json:"id" binding:"required"`
Status int `json:"status" binding:"oneof=0 1"`
}
req := &Req{}
err := ctx.ShouldBindJSON(req)
if err != nil {
fail(ctx, err)
return
}
mForward, err := getForwardById(req.Id)
if err != nil {
fail(ctx, err)
return
}
err = db.DB().Transaction(func(tx *gorm.DB) error {
err = tx.Table("forward").Where("id = ?", req.Id).Updates(map[string]any{
"status": req.Status,
}).Limit(1).Error
if err != nil {
return err
}
switch req.Status {
case 1:
err = forward.ListenerManager.Add(mForward)
if err != nil {
return err
}
default:
forward.ListenerManager.Remove(mForward)
}
return nil
})
if err != nil {
fail(ctx, err)
return
}
success(ctx, "ok")
}
type apiRes struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data"`
}
func success(ctx *gin.Context, data any) {
ctx.JSON(http.StatusOK, &apiRes{
Code: 0,
Message: "ok",
Data: data,
})
}
var ginTrans ut.Translator
func InitGinTrans() {
v := binding.Validator.Engine().(*validator.Validate)
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
nameSp := strings.SplitN(fld.Tag.Get("json"), ",", 2)
if len(nameSp) == 0 {
return fld.Name
}
name := nameSp[0]
if name == "-" {
return ""
}
if name != "" {
return name
}
return fld.Name
})
enT := en.New()
uni := ut.New(enT, enT)
tr, _ := uni.GetTranslator("en")
_ = enTr.RegisterDefaultTranslations(v, tr)
ginTrans = tr
}
func fail(ctx *gin.Context, err error) {
res := &apiRes{Code: 1}
switch ve := err.(type) {
case validator.ValidationErrors:
if len(ve) > 0 {
res.Message = ve[0].Translate(ginTrans)
} else {
res.Message = ve.Error()
}
default:
res.Message = err.Error()
}
ctx.JSON(http.StatusOK, res)
}
var emptySplitReg = regexp.MustCompile(`\s*\n\s*`)
func parseAddrList(addrStr string) ([]string, error) {
sp := emptySplitReg.Split(addrStr, -1)
if len(sp) == 0 {
return nil, fmt.Errorf("addr list is empty")
}
res := make([]string, 0)
for i, _ := range sp {
sp[i] = strings.TrimSpace(sp[i])
if sp[i] == "" {
continue
}
err := checkAddr(sp[i])
if err != nil {
return nil, err
}
res = append(res, sp[i])
}
return res, nil
}

3
frontend/.env Normal file
View File

@ -0,0 +1,3 @@
VITE_API_BASE_URL="/"
VITE_WEB_USER=""
VITE_WEB_PASSWORD=""

3
frontend/.env.dev Normal file
View File

@ -0,0 +1,3 @@
VITE_API_BASE_URL="http://localhost:28083/"
VITE_WEB_USER=""
VITE_WEB_PASSWORD=""

30
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
frontend/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

29
frontend/README.md Normal file
View File

@ -0,0 +1,29 @@
# frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

12
frontend/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>端口转发配置</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.js"></script>
</body>
</html>

8
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

2120
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
frontend/package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host --mode dev --port 15173",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.7",
"element-plus": "^2.8.4",
"pinia": "^2.1.7",
"vue": "^3.4.29",
"vue-router": "^4.3.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.5",
"sass": "^1.79.4",
"sass-loader": "^16.0.2",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.3.1"
}
}

47
frontend/src/App.vue Normal file
View File

@ -0,0 +1,47 @@
<template>
<div class="app-container">
<div class="content">
<RouterView/>
</div>
</div>
</template>
<script setup>
import {RouterView} from 'vue-router'
</script>
<style>
.text-danger {
color: #F56C6C!important;
}
</style>
<style lang="scss" scoped>
.app-container {
display: flex;
height: 100%;
width: 100%;
.menu {
height: 100%;
width: 12rem;
box-sizing: border-box;
}
.content {
flex: auto;
box-sizing: border-box;
padding: 0 0.8rem;
}
}
</style>

View File

@ -0,0 +1,77 @@
import axios from "axios";
import {ElMessage} from 'element-plus'
import {Loading} from "@/helper/loading.js";
const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 60 * 1000,
})
http.defaults.headers.post['Content-Type'] = "application/json"
const loadingObj = new Loading()
http.interceptors.request.use(
(config) => {
loadingObj.addLoading()
// console.log(config)
let user = import.meta.env.VITE_WEB_USER
let password = import.meta.env.VITE_WEB_PASSWORD
if (user && password) {
config.auth = {
username: user,
password: password
}
}
return config
// return new Promise((resolve, reject) => {
// resolve(config)
// })
},
err => {
return Promise.reject(err)
},
)
http.interceptors.response.use(
res => {
loadingObj.closeLoading()
if (res.data.code !== 0) {
ElMessage({
message: res.data.message,
type: 'error',
showClose: true,
})
return Promise.reject(res)
}
//
// ElMessage({
// message: 'Congrats, this is a success message.',
// type: 'success',
// })
return res
},
err => {
loadingObj.closeLoading()
ElMessage({
message: err,
type: 'error',
showClose: true,
})
return Promise.reject(err)
}
)
export default http

View File

@ -0,0 +1,36 @@
import {ElLoading} from 'element-plus'
export class Loading {
loadingCount = 0
loading = null
constructor() {
this.loadingCount = 0
}
initLoading = () => {
if (this.loading) {
this.loading.close()
}
this.loading = ElLoading.service({
fullscreen: true
})
}
addLoading = () => {
if (this.loadingCount === 0) {
this.initLoading()
}
this.loadingCount++
}
closeLoading = () => {
if (this.loadingCount > 0) {
if (this.loadingCount === 1) {
this.loading.close()
}
this.loadingCount--
}
}
}

13
frontend/src/main.js Normal file
View File

@ -0,0 +1,13 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

View File

@ -0,0 +1,14 @@
import {createRouter, createWebHashHistory, createWebHistory} from 'vue-router'
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue')
}
]
})
export default router

View File

@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

229
frontend/src/views/Home.vue Normal file
View File

@ -0,0 +1,229 @@
<template>
<div>
<div>
<h2>端口转发</h2>
</div>
<div style="width: 100%;">
<el-button type="success" @click="createForward"></el-button>
</div>
<div>
<el-table :data="data.list" style="width: 100%">
<el-table-column align="center" label="协议">
<template #default="scope">
<div v-if="scope.row.protocol === 1">
<el-tag type="success">UDP</el-tag>
</div>
<div v-else>
<el-tag type="primary">TCP</el-tag>
</div>
</template>
</el-table-column>
<el-table-column align="center" label="状态">
<template #default="scope">
<el-switch v-model="scope.row.status_bool" @change="switchStatus(scope.row)"/>
</template>
</el-table-column>
<el-table-column align="center" prop="name" label="名称"/>
<el-table-column align="center" prop="local_port" label="本地端口"/>
<el-table-column align="center" label="远程地址">
<template #default="scope">
<div style="white-space: pre">{{ formatList(scope.row.target_addr) }}</div>
</template>
</el-table-column>
<el-table-column align="center" label="操作">
<template #default="scope">
<el-button size="small" type="primary" @click="updateForward(scope.row)"></el-button>
<el-popconfirm title="Are you sure to delete this?" @confirm="deleteForward(scope.row)" :hide-after="0">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog
v-model="data.dialogVisible"
:title="data.form.id > 0 ? '修改': '添加'"
width="600px"
align-center
>
<div>
<el-form :model="data.form" label-width="auto">
<el-form-item label="协议">
<el-radio-group v-model="data.form.protocol">
<el-radio :value="0">TCP</el-radio>
<el-radio :value="1">UDP</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="名称">
<el-input v-model="data.form.name" placeholder="名称"/>
</el-form-item>
<el-form-item label="本地端口">
<el-input v-model="data.form.local_port" placeholder="本地监听端口"/>
</el-form-item>
<el-form-item label="远程地址">
<el-input type="textarea" :rows="5" v-model="data.form.target_addr" placeholder="一行一个, 格式 ip:端口, 例如 127.0.0.1:8080"/>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="data.dialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmConfig"></el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {onMounted, reactive} from "vue";
import http from "@/helper/http.js";
const data = reactive({
list: [],
dialogVisible: false,
form: {
protocol: 0,
}
})
onMounted(() => {
getList()
})
const createForward = () => {
data.dialogVisible = true
data.form = {
protocol: 0,
}
}
const updateForward = (item) => {
data.dialogVisible = true
data.form = JSON.parse(JSON.stringify(item))
data.form.target_addr = item.target_addr.join("\n")
}
const deleteForward = async (item) => {
await http.post("/Delete", {
id: item.id,
})
await getList()
}
const switchStatus = async (item) => {
console.log(item)
let req = {
id: item.id,
}
if (item.status_bool) {
req.status = 1
} else {
req.status = 0
}
try {
await http.post("/SwitchStatus", req)
await getList()
} catch (err) {
item.status_bool = !item.status_bool
console.log(err)
}
}
const getList = async () => {
let res = await http.get("/List")
data.list = res.data.data
for (const v of data.list) {
if (v.status === 1) {
v.status_bool = true
} else {
v.status_bool = false
}
}
console.log(res)
}
const confirmConfig = async () => {
console.log(data.form)
let res;
if (data.form.id > 0) {
res = await http.post("/Update", {
id: data.form.id,
name: data.form.name,
local_port: Number(data.form.local_port),
target_addr: data.form.target_addr,
protocol: data.form.protocol,
})
} else {
res = await http.post("/Create", {
name: data.form.name,
local_port: Number(data.form.local_port),
target_addr: data.form.target_addr,
protocol: data.form.protocol,
})
}
data.dialogVisible = false
getList()
console.log(res)
}
const setStatus = (item) => {
if (item.status_bool) {
} else {
}
}
const formatList = (list) => {
if (list.length === 0) {
return ""
}
return list.join("\n")
}
</script>

26
frontend/vite.config.js Normal file
View File

@ -0,0 +1,26 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
base: './',
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

42
go.mod
View File

@ -3,16 +3,42 @@ module proxyport
go 1.18 go 1.18
require ( require (
git.makemake.in/kzkzzzz/mycommon v0.0.0-20240719075030-85d75101130c git.makemake.in/kzkzzzz/mycommon v0.0.0-20240930075521-197476e80569
github.com/glebarez/sqlite v1.11.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
gorm.io/gorm v1.25.7
) )
require ( require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.10.0 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/libp2p/go-reuseport v0.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
@ -20,12 +46,22 @@ require (
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/viper v1.19.0 // indirect github.com/spf13/viper v1.19.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect go.uber.org/zap v1.27.0 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
) )

22
local.sh Normal file
View File

@ -0,0 +1,22 @@
#!/bin/bash
set -e
case $1 in
"backend")
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -o temp/proxyport.linux main.go
scp -P 5566 temp/proxyport.linux kzkzzzz@193.32.149.42:temp/proxyport/proxyport.linux
;;
"frontend")
cd frontend
npm run build
static_dir=/c/userdata/dev/devgo/my/proxyport/app/static
rm -rf $static_dir/assets
cp -rf dist/index.html $static_dir/view/
cp -rf dist/assets $static_dir/assets/
;;
*)
echo 'other'
;;
esac

60
main.go
View File

@ -1,23 +1,59 @@
package main package main
import ( import (
"git.makemake.in/kzkzzzz/mycommon/myconf" "context"
"flag"
"git.makemake.in/kzkzzzz/mycommon/mylog" "git.makemake.in/kzkzzzz/mycommon/mylog"
"github.com/spf13/pflag" "os"
"proxyport/app" "os/signal"
"proxyport/app/db"
"proxyport/app/forward"
"proxyport/app/web"
"sync"
"syscall"
)
var (
logLevel string
) )
func main() { func main() {
pflag.String("conf", "config.toml", "config file path") flag.StringVar(&logLevel, "log_level", "debug", "log level")
pflag.String("log.level", "info", "log level") flag.StringVar(&web.Config.ListenAddr, "listen_addr", ":28083", "web port")
myconf.LoadFlag() flag.StringVar(&web.Config.User, "user", "", "web user")
flag.StringVar(&web.Config.Password, "password", "", "web password")
flag.Parse()
myconf.LoadFile(myconf.Conf().GetString("conf")) mylog.SetLogLevel(logLevel)
mylog.Init()
config := mylog.DefaultConfig ctx, cancel := context.WithCancel(context.Background())
config.Level = myconf.Conf().GetString("log.level") defer cancel()
mylog.Init("", config)
defer mylog.Flush()
app.Run() db.InitDB()
wg := &sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
mylog.Warnf("catch signal: %v", <-ch)
cancel()
}()
go func() {
defer wg.Done()
forward.ListenerManager.Start(ctx)
}()
go func() {
defer wg.Done()
web.Start(ctx)
}()
wg.Wait()
} }