update
							parent
							
								
									1b27f3b2ab
								
							
						
					
					
						commit
						8239477e53
					
				| 
						 | 
				
			
			@ -4,3 +4,4 @@ go.sum
 | 
			
		|||
config.toml
 | 
			
		||||
temp
 | 
			
		||||
logs
 | 
			
		||||
*.db
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
)`
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										126
									
								
								app/proxy.go
								
								
								
								
							
							
						
						
									
										126
									
								
								app/proxy.go
								
								
								
								
							| 
						 | 
				
			
			@ -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
											
										
									
								
							| 
						 | 
				
			
			@ -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/*")))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
VITE_API_BASE_URL="/"
 | 
			
		||||
VITE_WEB_USER=""
 | 
			
		||||
VITE_WEB_PASSWORD=""
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
VITE_API_BASE_URL="http://localhost:28083/"
 | 
			
		||||
VITE_WEB_USER=""
 | 
			
		||||
VITE_WEB_PASSWORD=""
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,3 @@
 | 
			
		|||
{
 | 
			
		||||
  "recommendations": ["Vue.volar"]
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
```
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "exclude": ["node_modules", "dist"]
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -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"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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--
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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')
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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 }
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			@ -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
								
								
								
								
							
							
						
						
									
										42
									
								
								go.mod
								
								
								
								
							| 
						 | 
				
			
			@ -3,16 +3,42 @@ module proxyport
 | 
			
		|||
go 1.18
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
	gorm.io/gorm v1.25.7
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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/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/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/mattn/go-isatty v0.0.20 // 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 | 
			
		||||
	github.com/sagikazarmark/locafero v0.4.0 // indirect
 | 
			
		||||
	github.com/sagikazarmark/slog-shim v0.1.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/viper v1.19.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/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/sys v0.18.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.14.0 // indirect
 | 
			
		||||
	golang.org/x/net v0.25.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/natefinch/lumberjack.v2 v2.2.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
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
								
								
								
								
							
							
						
						
									
										60
									
								
								main.go
								
								
								
								
							| 
						 | 
				
			
			@ -1,23 +1,59 @@
 | 
			
		|||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"git.makemake.in/kzkzzzz/mycommon/myconf"
 | 
			
		||||
	"context"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"git.makemake.in/kzkzzzz/mycommon/mylog"
 | 
			
		||||
	"github.com/spf13/pflag"
 | 
			
		||||
	"proxyport/app"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"proxyport/app/db"
 | 
			
		||||
	"proxyport/app/forward"
 | 
			
		||||
	"proxyport/app/web"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"syscall"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	logLevel string
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	pflag.String("conf", "config.toml", "config file path")
 | 
			
		||||
	pflag.String("log.level", "info", "log level")
 | 
			
		||||
	myconf.LoadFlag()
 | 
			
		||||
	flag.StringVar(&logLevel, "log_level", "debug", "log level")
 | 
			
		||||
	flag.StringVar(&web.Config.ListenAddr, "listen_addr", ":28083", "web port")
 | 
			
		||||
	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
 | 
			
		||||
	config.Level = myconf.Conf().GetString("log.level")
 | 
			
		||||
	mylog.Init("", config)
 | 
			
		||||
	defer mylog.Flush()
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 | 
			
		||||
	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()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue