Browse Source

Restructured TCP proxy into streamproxy module

Toby Chui 9 months ago
parent
commit
09012a2451

+ 7 - 8
api.go

@@ -141,14 +141,13 @@ func initAPIs() {
 	authRouter.HandleFunc("/api/gan/members/delete", ganManager.HandleMemberDelete)
 
 	//TCP Proxy
-	authRouter.HandleFunc("/api/tcpprox/config/add", tcpProxyManager.HandleAddProxyConfig)
-	authRouter.HandleFunc("/api/tcpprox/config/edit", tcpProxyManager.HandleEditProxyConfigs)
-	authRouter.HandleFunc("/api/tcpprox/config/list", tcpProxyManager.HandleListConfigs)
-	authRouter.HandleFunc("/api/tcpprox/config/start", tcpProxyManager.HandleStartProxy)
-	authRouter.HandleFunc("/api/tcpprox/config/stop", tcpProxyManager.HandleStopProxy)
-	authRouter.HandleFunc("/api/tcpprox/config/delete", tcpProxyManager.HandleRemoveProxy)
-	authRouter.HandleFunc("/api/tcpprox/config/status", tcpProxyManager.HandleGetProxyStatus)
-	authRouter.HandleFunc("/api/tcpprox/config/validate", tcpProxyManager.HandleConfigValidate)
+	authRouter.HandleFunc("/api/streamprox/config/add", streamProxyManager.HandleAddProxyConfig)
+	authRouter.HandleFunc("/api/streamprox/config/edit", streamProxyManager.HandleEditProxyConfigs)
+	authRouter.HandleFunc("/api/streamprox/config/list", streamProxyManager.HandleListConfigs)
+	authRouter.HandleFunc("/api/streamprox/config/start", streamProxyManager.HandleStartProxy)
+	authRouter.HandleFunc("/api/streamprox/config/stop", streamProxyManager.HandleStopProxy)
+	authRouter.HandleFunc("/api/streamprox/config/delete", streamProxyManager.HandleRemoveProxy)
+	authRouter.HandleFunc("/api/streamprox/config/status", streamProxyManager.HandleGetProxyStatus)
 
 	//mDNS APIs
 	authRouter.HandleFunc("/api/mdns/list", HandleMdnsListing)

+ 2 - 2
main.go

@@ -28,7 +28,7 @@ import (
 	"imuslab.com/zoraxy/mod/sshprox"
 	"imuslab.com/zoraxy/mod/statistic"
 	"imuslab.com/zoraxy/mod/statistic/analytic"
-	"imuslab.com/zoraxy/mod/tcpprox"
+	"imuslab.com/zoraxy/mod/streamproxy"
 	"imuslab.com/zoraxy/mod/tlscert"
 	"imuslab.com/zoraxy/mod/uptime"
 	"imuslab.com/zoraxy/mod/utils"
@@ -79,7 +79,7 @@ var (
 	mdnsScanner        *mdns.MDNSHost          //mDNS discovery services
 	ganManager         *ganserv.NetworkManager //Global Area Network Manager
 	webSshManager      *sshprox.Manager        //Web SSH connection service
-	tcpProxyManager    *tcpprox.Manager        //TCP Proxy Manager
+	streamProxyManager *streamproxy.Manager    //Stream Proxy Manager for TCP / UDP forwarding
 	acmeHandler        *acme.ACMEHandler       //Handler for ACME Certificate renew
 	acmeAutoRenewer    *acme.AutoRenewer       //Handler for ACME auto renew ticking
 	staticWebServer    *webserv.WebServer      //Static web server for hosting simple stuffs

+ 19 - 60
mod/tcpprox/handler.go → mod/streamproxy/handler.go

@@ -1,9 +1,10 @@
-package tcpprox
+package streamproxy
 
 import (
 	"encoding/json"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"imuslab.com/zoraxy/mod/utils"
 )
@@ -22,13 +23,13 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	portA, err := utils.PostPara(r, "porta")
+	listenAddr, err := utils.PostPara(r, "listenAddr")
 	if err != nil {
 		utils.SendErrorResponse(w, "first address cannot be empty")
 		return
 	}
 
-	portB, err := utils.PostPara(r, "portb")
+	proxyAddr, err := utils.PostPara(r, "proxyAddr")
 	if err != nil {
 		utils.SendErrorResponse(w, "second address cannot be empty")
 		return
@@ -44,27 +45,17 @@ func (m *Manager) HandleAddProxyConfig(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	modeValue := ProxyMode_Transport
-	mode, err := utils.PostPara(r, "mode")
-	if err != nil || mode == "" {
-		utils.SendErrorResponse(w, "no mode given")
-	} else if mode == "listen" {
-		modeValue = ProxyMode_Listen
-	} else if mode == "transport" {
-		modeValue = ProxyMode_Transport
-	} else if mode == "starter" {
-		modeValue = ProxyMode_Starter
-	} else {
-		utils.SendErrorResponse(w, "invalid mode given. Only support listen / transport / starter")
-	}
+	useTCP, _ := utils.PostBool(r, "useTCP")
+	useUDP, _ := utils.PostBool(r, "useUDP")
 
 	//Create the target config
 	newConfigUUID := m.NewConfig(&ProxyRelayOptions{
-		Name:    name,
-		PortA:   portA,
-		PortB:   portB,
-		Timeout: timeout,
-		Mode:    modeValue,
+		Name:          name,
+		ListeningAddr: strings.TrimSpace(listenAddr),
+		ProxyAddr:     strings.TrimSpace(proxyAddr),
+		Timeout:       timeout,
+		UseTCP:        useTCP,
+		UseUDP:        useUDP,
 	})
 
 	js, _ := json.Marshal(newConfigUUID)
@@ -80,22 +71,10 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
 	}
 
 	newName, _ := utils.PostPara(r, "name")
-	newPortA, _ := utils.PostPara(r, "porta")
-	newPortB, _ := utils.PostPara(r, "portb")
-	newModeStr, _ := utils.PostPara(r, "mode")
-	newMode := -1
-	if newModeStr != "" {
-		if newModeStr == "listen" {
-			newMode = 0
-		} else if newModeStr == "transport" {
-			newMode = 1
-		} else if newModeStr == "starter" {
-			newMode = 2
-		} else {
-			utils.SendErrorResponse(w, "invalid new mode value")
-			return
-		}
-	}
+	listenAddr, _ := utils.PostPara(r, "listenAddr")
+	proxyAddr, _ := utils.PostPara(r, "proxyAddr")
+	useTCP, _ := utils.PostBool(r, "useTCP")
+	useUDP, _ := utils.PostBool(r, "useUDP")
 
 	newTimeoutStr, _ := utils.PostPara(r, "timeout")
 	newTimeout := -1
@@ -108,7 +87,7 @@ func (m *Manager) HandleEditProxyConfigs(w http.ResponseWriter, r *http.Request)
 	}
 
 	// Call the EditConfig method to modify the configuration
-	err = m.EditConfig(configUUID, newName, newPortA, newPortB, newMode, newTimeout)
+	err = m.EditConfig(configUUID, newName, listenAddr, proxyAddr, useTCP, useUDP, newTimeout)
 	if err != nil {
 		utils.SendErrorResponse(w, err.Error())
 		return
@@ -158,6 +137,7 @@ func (m *Manager) HandleStopProxy(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if !targetProxyConfig.IsRunning() {
+		targetProxyConfig.Running = false
 		utils.SendErrorResponse(w, "target proxy service is not running")
 		return
 	}
@@ -180,6 +160,7 @@ func (m *Manager) HandleRemoveProxy(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if targetProxyConfig.IsRunning() {
+		targetProxyConfig.Running = false
 		utils.SendErrorResponse(w, "Service is running")
 		return
 	}
@@ -209,25 +190,3 @@ func (m *Manager) HandleGetProxyStatus(w http.ResponseWriter, r *http.Request) {
 	js, _ := json.Marshal(targetConfig)
 	utils.SendJSONResponse(w, string(js))
 }
-
-func (m *Manager) HandleConfigValidate(w http.ResponseWriter, r *http.Request) {
-	uuid, err := utils.GetPara(r, "uuid")
-	if err != nil {
-		utils.SendErrorResponse(w, "invalid uuid given")
-		return
-	}
-
-	targetConfig, err := m.GetConfigByUUID(uuid)
-	if err != nil {
-		utils.SendErrorResponse(w, err.Error())
-		return
-	}
-
-	err = targetConfig.ValidateConfigs()
-	if err != nil {
-		utils.SendErrorResponse(w, err.Error())
-		return
-	}
-
-	utils.SendOK(w)
-}

+ 107 - 42
mod/tcpprox/tcpprox.go → mod/streamproxy/streamproxy.go

@@ -1,7 +1,8 @@
-package tcpprox
+package streamproxy
 
 import (
 	"errors"
+	"log"
 	"net"
 	"sync"
 	"sync/atomic"
@@ -18,30 +19,27 @@ import (
 	connection
 */
 
-const (
-	ProxyMode_Listen    = 0
-	ProxyMode_Transport = 1
-	ProxyMode_Starter   = 2
-	ProxyMode_UDP       = 3
-)
-
 type ProxyRelayOptions struct {
-	Name    string
-	PortA   string
-	PortB   string
-	Timeout int
-	Mode    int
+	Name          string
+	ListeningAddr string
+	ProxyAddr     string
+	Timeout       int
+	UseTCP        bool
+	UseUDP        bool
 }
 
 type ProxyRelayConfig struct {
 	UUID                        string       //A UUIDv4 representing this config
 	Name                        string       //Name of the config
-	Running                     bool         //If the service is running
-	PortA                       string       //Ports A (config depends on mode)
-	PortB                       string       //Ports B (config depends on mode)
-	Mode                        int          //Operation Mode
+	Running                     bool         //Status, read only
+	AutoStart                   bool         //If the service suppose to started automatically
+	ListeningAddress            string       //Listening Address, usually 127.0.0.1:port
+	ProxyTargetAddr             string       //Proxy target address
+	UseTCP                      bool         //Enable TCP proxy
+	UseUDP                      bool         //Enable UDP proxy
 	Timeout                     int          //Timeout for connection in sec
-	stopChan                    chan bool    //Stop channel to stop the listener
+	tcpStopChan                 chan bool    //Stop channel for TCP listener
+	udpStopChan                 chan bool    //Stop channel for UDP listener
 	aTobAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from A to B
 	bToaAccumulatedByteTransfer atomic.Int64 //Accumulated byte transfer from B to A
 	udpClientMap                sync.Map     //map storing the UDP client-server connections
@@ -64,7 +62,7 @@ type Manager struct {
 
 }
 
-func NewTCProxy(options *Options) *Manager {
+func NewStreamProxy(options *Options) *Manager {
 	options.Database.NewTable("tcprox")
 
 	//Load relay configs from db
@@ -108,12 +106,13 @@ func (m *Manager) NewConfig(config *ProxyRelayOptions) string {
 	thisConfig := ProxyRelayConfig{
 		UUID:                        configUUID,
 		Name:                        config.Name,
-		Running:                     false,
-		PortA:                       config.PortA,
-		PortB:                       config.PortB,
-		Mode:                        config.Mode,
+		ListeningAddress:            config.ListeningAddr,
+		ProxyTargetAddr:             config.ProxyAddr,
+		UseTCP:                      config.UseTCP,
+		UseUDP:                      config.UseUDP,
 		Timeout:                     config.Timeout,
-		stopChan:                    nil,
+		tcpStopChan:                 nil,
+		udpStopChan:                 nil,
 		aTobAccumulatedByteTransfer: aAcc,
 		bToaAccumulatedByteTransfer: bAcc,
 		udpClientMap:                sync.Map{},
@@ -135,7 +134,7 @@ func (m *Manager) GetConfigByUUID(configUUID string) (*ProxyRelayConfig, error)
 }
 
 // Edit the config based on config UUID, leave empty for unchange fields
-func (m *Manager) EditConfig(configUUID string, newName string, newPortA string, newPortB string, newMode int, newTimeout int) error {
+func (m *Manager) EditConfig(configUUID string, newName string, newListeningAddr string, newProxyAddr string, useTCP bool, useUDP bool, newTimeout int) error {
 	// Find the config with the specified UUID
 	foundConfig, err := m.GetConfigByUUID(configUUID)
 	if err != nil {
@@ -146,18 +145,16 @@ func (m *Manager) EditConfig(configUUID string, newName string, newPortA string,
 	if newName != "" {
 		foundConfig.Name = newName
 	}
-	if newPortA != "" {
-		foundConfig.PortA = newPortA
+	if newListeningAddr != "" {
+		foundConfig.ListeningAddress = newListeningAddr
 	}
-	if newPortB != "" {
-		foundConfig.PortB = newPortB
-	}
-	if newMode != -1 {
-		if newMode > 2 || newMode < 0 {
-			return errors.New("invalid mode given")
-		}
-		foundConfig.Mode = newMode
+	if newProxyAddr != "" {
+		foundConfig.ProxyTargetAddr = newProxyAddr
 	}
+
+	foundConfig.UseTCP = useTCP
+	foundConfig.UseUDP = useUDP
+
 	if newTimeout != -1 {
 		if newTimeout < 0 {
 			return errors.New("invalid timeout value given")
@@ -165,13 +162,6 @@ func (m *Manager) EditConfig(configUUID string, newName string, newPortA string,
 		foundConfig.Timeout = newTimeout
 	}
 
-	/*
-		err = foundConfig.ValidateConfigs()
-		if err != nil {
-			return err
-		}
-	*/
-
 	m.SaveConfigToDatabase()
 
 	return nil
@@ -192,3 +182,78 @@ func (m *Manager) RemoveConfig(configUUID string) error {
 func (m *Manager) SaveConfigToDatabase() {
 	m.Options.Database.Write("tcprox", "rules", m.Configs)
 }
+
+/*
+	Config Functions
+*/
+
+// Start a proxy if stopped
+func (c *ProxyRelayConfig) Start() error {
+	if c.IsRunning() {
+		c.Running = true
+		return errors.New("proxy already running")
+	}
+
+	// Create a stopChan to control the loop
+	tcpStopChan := make(chan bool)
+	c.tcpStopChan = tcpStopChan
+
+	udpStopChan := make(chan bool)
+	c.udpStopChan = udpStopChan
+
+	//Start the proxy service
+	if c.UseUDP {
+		go func() {
+			if !c.UseTCP {
+				//By default running state shows TCP proxy. If TCP is not in use, UDP is shown instead
+				c.Running = true
+			}
+			err := c.ForwardUDP(c.ListeningAddress, c.ProxyTargetAddr, udpStopChan)
+			if err != nil {
+				if !c.UseTCP {
+					c.Running = false
+				}
+				log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
+			}
+		}()
+	}
+
+	if c.UseTCP {
+		go func() {
+			//Default to transport mode
+			c.Running = true
+			err := c.Port2host(c.ListeningAddress, c.ProxyTargetAddr, tcpStopChan)
+			if err != nil {
+				c.Running = false
+				log.Println("[TCP] Error starting stream proxy " + c.Name + "(" + c.UUID + "): " + err.Error())
+			}
+		}()
+	}
+
+	//Successfully spawned off the proxy routine
+
+	return nil
+}
+
+// Stop a running proxy if running
+func (c *ProxyRelayConfig) IsRunning() bool {
+	return c.tcpStopChan != nil || c.udpStopChan != nil
+}
+
+// Stop a running proxy if running
+func (c *ProxyRelayConfig) Stop() {
+	log.Println("[PROXY] Stopping Stream Proxy " + c.Name)
+
+	if c.udpStopChan != nil {
+		c.udpStopChan <- true
+		c.udpStopChan = nil
+	}
+
+	if c.tcpStopChan != nil {
+		c.tcpStopChan <- true
+		c.tcpStopChan = nil
+	}
+
+	log.Println("[PROXY] Stopped Stream Proxy " + c.Name)
+	c.Running = false
+}

+ 4 - 4
mod/tcpprox/tcpprox_test.go → mod/streamproxy/streamproxy_test.go

@@ -1,10 +1,10 @@
-package tcpprox_test
+package streamproxy_test
 
 import (
 	"testing"
 	"time"
 
-	"imuslab.com/zoraxy/mod/tcpprox"
+	"imuslab.com/zoraxy/mod/streamproxy"
 )
 
 func TestPort2Port(t *testing.T) {
@@ -12,7 +12,7 @@ func TestPort2Port(t *testing.T) {
 	stopChan := make(chan bool)
 
 	// Create a ProxyRelayConfig with dummy values
-	config := &tcpprox.ProxyRelayConfig{
+	config := &streamproxy.ProxyRelayConfig{
 		Timeout: 1,
 	}
 
@@ -36,7 +36,7 @@ func TestPort2Port(t *testing.T) {
 	time.Sleep(1 * time.Second)
 
 	// If the goroutine is still running, it means it did not stop as expected
-	if config.Running {
+	if config.IsRunning() {
 		t.Errorf("port2port did not stop as expected")
 	}
 

+ 146 - 0
mod/streamproxy/tcpprox.go

@@ -0,0 +1,146 @@
+package streamproxy
+
+import (
+	"errors"
+	"io"
+	"log"
+	"net"
+	"strconv"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+)
+
+func isValidIP(ip string) bool {
+	parsedIP := net.ParseIP(ip)
+	return parsedIP != nil
+}
+
+func isValidPort(port string) bool {
+	portInt, err := strconv.Atoi(port)
+	if err != nil {
+		return false
+	}
+
+	if portInt < 1 || portInt > 65535 {
+		return false
+	}
+
+	return true
+}
+
+func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *atomic.Int64) {
+	n, err := io.Copy(conn1, conn2)
+	if err != nil {
+		return
+	}
+	accumulator.Add(n) //Add to accumulator
+	conn1.Close()
+	log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]")
+	//conn2.Close()
+	//log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]")
+	wg.Done()
+}
+
+func forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) {
+	log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
+	var wg sync.WaitGroup
+	// wait tow goroutines
+	wg.Add(2)
+	go connCopy(conn1, conn2, &wg, aTob)
+	go connCopy(conn2, conn1, &wg, bToa)
+	//blocking when the wg is locked
+	wg.Wait()
+}
+
+func (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
+	conn, err := listener.Accept()
+	if err != nil {
+		return nil, err
+	}
+
+	//Check if connection in blacklist or whitelist
+	if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
+		if !c.parent.Options.AccessControlHandler(conn) {
+			time.Sleep(300 * time.Millisecond)
+			conn.Close()
+			log.Println("[x]", "Connection from "+addr.IP.String()+" rejected by access control policy")
+			return nil, errors.New("Connection from " + addr.IP.String() + " rejected by access control policy")
+		}
+	}
+
+	log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]")
+	return conn, err
+}
+
+func startListener(address string) (net.Listener, error) {
+	log.Println("[+]", "try to start server on:["+address+"]")
+	server, err := net.Listen("tcp", address)
+	if err != nil {
+		return nil, errors.New("listen address [" + address + "] faild")
+	}
+	log.Println("[√]", "start listen at address:["+address+"]")
+	return server, nil
+}
+
+/*
+	Forwarder Functions
+*/
+
+/*
+portA -> server
+server -> portB
+*/
+func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, stopChan chan bool) error {
+	listenerStartingAddr := allowPort
+	if isValidPort(allowPort) {
+		//number only, e.g. 8080
+		listenerStartingAddr = "0.0.0.0:" + allowPort
+	} else if strings.HasPrefix(allowPort, ":") && isValidPort(allowPort[1:]) {
+		//port number starting with :, e.g. :8080
+		listenerStartingAddr = "0.0.0.0" + allowPort
+	}
+
+	server, err := startListener(listenerStartingAddr)
+	if err != nil {
+		return err
+	}
+
+	targetAddress = strings.TrimSpace(targetAddress)
+
+	//Start stop handler
+	go func() {
+		<-stopChan
+		log.Println("[x]", "Received stop signal. Exiting Port to Host forwarder")
+		server.Close()
+	}()
+
+	//Start blocking loop for accepting connections
+	for {
+		conn, err := c.accept(server)
+		if err != nil {
+			if errors.Is(err, net.ErrClosed) {
+				//Terminate by stop chan. Exit listener loop
+				return nil
+			}
+			//Connection error. Retry
+			continue
+		}
+
+		go func(targetAddress string) {
+			log.Println("[+]", "start connect host:["+targetAddress+"]")
+			target, err := net.Dial("tcp", targetAddress)
+			if err != nil {
+				// temporarily unavailable, don't use fatal.
+				log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", c.Timeout, "seconds. ")
+				conn.Close()
+				log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]")
+				time.Sleep(time.Duration(c.Timeout) * time.Second)
+				return
+			}
+			log.Println("[→]", "connect target address ["+targetAddress+"] success.")
+			forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
+		}(targetAddress)
+	}
+}

+ 11 - 2
mod/tcpprox/udpprox.go → mod/streamproxy/udpprox.go

@@ -1,9 +1,10 @@
-package tcpprox
+package streamproxy
 
 import (
 	"errors"
 	"log"
 	"net"
+	"strings"
 	"time"
 )
 
@@ -84,7 +85,15 @@ func (c *ProxyRelayConfig) CloseAllUDPConnections() {
 func (c *ProxyRelayConfig) ForwardUDP(address1, address2 string, stopChan chan bool) error {
 	//By default the incoming listen Address is int
 	//We need to add the loopback address into it
-	address1 = "127.0.0.1:" + address1
+	if isValidPort(address1) {
+		//Port number only. Missing the : in front
+		address1 = ":" + address1
+	}
+	if strings.HasPrefix(address1, ":") {
+		//Prepend 127.0.0.1 to the address
+		address1 = "127.0.0.1" + address1
+	}
+
 	lisener, targetAddr, err := initUDPConnections(address1, address2)
 	if err != nil {
 		return err

+ 0 - 358
mod/tcpprox/conn.go

@@ -1,358 +0,0 @@
-package tcpprox
-
-import (
-	"errors"
-	"io"
-	"log"
-	"net"
-	"strconv"
-	"sync"
-	"sync/atomic"
-	"time"
-)
-
-func isValidIP(ip string) bool {
-	parsedIP := net.ParseIP(ip)
-	return parsedIP != nil
-}
-
-func isValidPort(port string) bool {
-	portInt, err := strconv.Atoi(port)
-	if err != nil {
-		return false
-	}
-
-	if portInt < 1 || portInt > 65535 {
-		return false
-	}
-
-	return true
-}
-
-func isReachable(target string) bool {
-	timeout := time.Duration(2 * time.Second) // Set the timeout value as per your requirement
-	conn, err := net.DialTimeout("tcp", target, timeout)
-	if err != nil {
-		return false
-	}
-	defer conn.Close()
-	return true
-}
-
-func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup, accumulator *atomic.Int64) {
-	n, err := io.Copy(conn1, conn2)
-	if err != nil {
-		//Add to accumulator
-		accumulator.Add(n)
-	}
-	conn1.Close()
-	log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]")
-	//conn2.Close()
-	//log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]")
-	wg.Done()
-}
-
-func forward(conn1 net.Conn, conn2 net.Conn, aTob *atomic.Int64, bToa *atomic.Int64) {
-	log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
-	var wg sync.WaitGroup
-	// wait tow goroutines
-	wg.Add(2)
-	go connCopy(conn1, conn2, &wg, aTob)
-	go connCopy(conn2, conn1, &wg, bToa)
-	//blocking when the wg is locked
-	wg.Wait()
-}
-
-func (c *ProxyRelayConfig) accept(listener net.Listener) (net.Conn, error) {
-
-	conn, err := listener.Accept()
-	if err != nil {
-		return nil, err
-	}
-
-	//Check if connection in blacklist or whitelist
-	if addr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
-		if !c.parent.Options.AccessControlHandler(conn) {
-			time.Sleep(300 * time.Millisecond)
-			conn.Close()
-			log.Println("[x]", "Connection from "+addr.IP.String()+" rejected by access control policy")
-			return nil, errors.New("Connection from " + addr.IP.String() + " rejected by access control policy")
-		}
-	}
-
-	log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]")
-	return conn, err
-}
-
-func startListener(address string) (net.Listener, error) {
-	log.Println("[+]", "try to start server on:["+address+"]")
-	server, err := net.Listen("tcp", address)
-	if err != nil {
-		return nil, errors.New("listen address [" + address + "] faild")
-	}
-	log.Println("[√]", "start listen at address:["+address+"]")
-	return server, nil
-}
-
-/*
-	Config Functions
-*/
-
-// Config validator
-func (c *ProxyRelayConfig) ValidateConfigs() error {
-	if c.Mode == ProxyMode_Transport {
-		//Port2Host: PortA int, PortB string
-		if !isValidPort(c.PortA) {
-			return errors.New("first address must be a valid port number")
-		}
-
-		if !isReachable(c.PortB) {
-			return errors.New("second address is unreachable")
-		}
-		return nil
-
-	} else if c.Mode == ProxyMode_Listen {
-		//Port2Port: Both port are port number
-		if !isValidPort(c.PortA) {
-			return errors.New("first address is not a valid port number")
-		}
-
-		if !isValidPort(c.PortB) {
-			return errors.New("second address is not a valid port number")
-		}
-
-		return nil
-	} else if c.Mode == ProxyMode_Starter {
-		//Host2Host: Both have to be hosts
-		if !isReachable(c.PortA) {
-			return errors.New("first address is unreachable")
-		}
-
-		if !isReachable(c.PortB) {
-			return errors.New("second address is unreachable")
-		}
-
-		return nil
-	} else if c.Mode == ProxyMode_UDP {
-		//UDP
-		if !isValidPort(c.PortA) {
-			return errors.New("first address must be a valid port number")
-		}
-
-		return nil
-	}
-
-	return errors.New("invalid mode given")
-
-}
-
-// Start a proxy if stopped
-func (c *ProxyRelayConfig) Start() error {
-	if c.Running {
-		return errors.New("proxy already running")
-	}
-
-	// Create a stopChan to control the loop
-	stopChan := make(chan bool)
-	c.stopChan = stopChan
-
-	//Validate configs
-	err := c.ValidateConfigs()
-	if err != nil {
-		return err
-	}
-
-	//Start the proxy service
-	go func() {
-		c.Running = true
-		if c.Mode == ProxyMode_Transport {
-			err = c.Port2host(c.PortA, c.PortB, stopChan)
-		} else if c.Mode == ProxyMode_Listen {
-			err = c.Port2port(c.PortA, c.PortB, stopChan)
-		} else if c.Mode == ProxyMode_Starter {
-			err = c.Host2host(c.PortA, c.PortB, stopChan)
-		} else if c.Mode == ProxyMode_UDP {
-			err = c.ForwardUDP(c.PortA, c.PortB, stopChan)
-		}
-		if err != nil {
-			c.Running = false
-			log.Println("Error starting proxy service " + c.Name + "(" + c.UUID + "): " + err.Error())
-		}
-	}()
-
-	//Successfully spawned off the proxy routine
-	return nil
-}
-
-// Stop a running proxy if running
-func (c *ProxyRelayConfig) IsRunning() bool {
-	return c.Running || c.stopChan != nil
-}
-
-// Stop a running proxy if running
-func (c *ProxyRelayConfig) Stop() {
-	log.Println("[PROXY] Stopping Stream Proxy " + c.Name)
-	if c.Running || c.stopChan != nil {
-		c.stopChan <- true
-		time.Sleep(300 * time.Millisecond)
-		c.stopChan = nil
-		c.Running = false
-	}
-	log.Println("[PROXY] Stopped Stream Proxy " + c.Name)
-}
-
-/*
-	Forwarder Functions
-*/
-
-/*
-portA -> server
-portB -> server
-*/
-func (c *ProxyRelayConfig) Port2port(port1 string, port2 string, stopChan chan bool) error {
-	//Trim the Prefix of : if exists
-	listen1, err := startListener("0.0.0.0:" + port1)
-	if err != nil {
-		return err
-	}
-	listen2, err := startListener("0.0.0.0:" + port2)
-	if err != nil {
-		return err
-	}
-
-	log.Println("[√]", "listen port:", port1, "and", port2, "success. waiting for client...")
-	c.Running = true
-
-	go func() {
-		<-stopChan
-		log.Println("[x]", "Received stop signal. Exiting Port to Port forwarder")
-		c.Running = false
-		listen1.Close()
-		listen2.Close()
-	}()
-
-	for {
-		conn1, err := c.accept(listen1)
-		if err != nil {
-			if !c.Running {
-				return nil
-			}
-			continue
-		}
-
-		conn2, err := c.accept(listen2)
-		if err != nil {
-			if !c.Running {
-				return nil
-			}
-			continue
-		}
-
-		if conn1 == nil || conn2 == nil {
-			log.Println("[x]", "accept client faild. retry in ", c.Timeout, " seconds. ")
-			time.Sleep(time.Duration(c.Timeout) * time.Second)
-			continue
-		}
-		go forward(conn1, conn2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
-	}
-}
-
-/*
-portA -> server
-server -> portB
-*/
-func (c *ProxyRelayConfig) Port2host(allowPort string, targetAddress string, stopChan chan bool) error {
-	server, err := startListener("0.0.0.0:" + allowPort)
-	if err != nil {
-		return err
-	}
-
-	//Start stop handler
-	go func() {
-		<-stopChan
-		log.Println("[x]", "Received stop signal. Exiting Port to Host forwarder")
-		c.Running = false
-		server.Close()
-	}()
-
-	//Start blocking loop for accepting connections
-	for {
-		conn, err := c.accept(server)
-		if conn == nil || err != nil {
-			if !c.Running {
-				//Terminate by stop chan. Exit listener loop
-				return nil
-			}
-
-			//Connection error. Retry
-			continue
-		}
-
-		go func(targetAddress string) {
-			log.Println("[+]", "start connect host:["+targetAddress+"]")
-			target, err := net.Dial("tcp", targetAddress)
-			if err != nil {
-				// temporarily unavailable, don't use fatal.
-				log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", c.Timeout, "seconds. ")
-				conn.Close()
-				log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]")
-				time.Sleep(time.Duration(c.Timeout) * time.Second)
-				return
-			}
-			log.Println("[→]", "connect target address ["+targetAddress+"] success.")
-			forward(target, conn, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
-		}(targetAddress)
-	}
-}
-
-/*
-server -> portA
-server -> portB
-*/
-func (c *ProxyRelayConfig) Host2host(address1, address2 string, stopChan chan bool) error {
-	c.Running = true
-	go func() {
-		<-stopChan
-		log.Println("[x]", "Received stop signal. Exiting Host to Host forwarder")
-		c.Running = false
-	}()
-
-	for c.Running {
-		log.Println("[+]", "try to connect host:["+address1+"] and ["+address2+"]")
-		var host1, host2 net.Conn
-		var err error
-		for {
-			d := net.Dialer{Timeout: time.Duration(c.Timeout)}
-			host1, err = d.Dial("tcp", address1)
-			if err == nil {
-				log.Println("[→]", "connect ["+address1+"] success.")
-				break
-			} else {
-				log.Println("[x]", "connect target address ["+address1+"] faild. retry in ", c.Timeout, " seconds. ")
-				time.Sleep(time.Duration(c.Timeout) * time.Second)
-			}
-
-			if !c.Running {
-				return nil
-			}
-		}
-		for {
-			d := net.Dialer{Timeout: time.Duration(c.Timeout)}
-			host2, err = d.Dial("tcp", address2)
-			if err == nil {
-				log.Println("[→]", "connect ["+address2+"] success.")
-				break
-			} else {
-				log.Println("[x]", "connect target address ["+address2+"] faild. retry in ", c.Timeout, " seconds. ")
-				time.Sleep(time.Duration(c.Timeout) * time.Second)
-			}
-
-			if !c.Running {
-				return nil
-			}
-		}
-		go forward(host1, host2, &c.aTobAccumulatedByteTransfer, &c.bToaAccumulatedByteTransfer)
-	}
-
-	return nil
-}

+ 0 - 289
mod/tcpprox/nb.go.ref

@@ -1,289 +0,0 @@
-package tcpprox
-
-import (
-	"fmt"
-	"io"
-	"log"
-	"net"
-	"os"
-	"regexp"
-	"strconv"
-	"strings"
-	"sync"
-	"time"
-)
-
-const timeout = 5
-
-func main() {
-	//log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
-	log.SetFlags(log.Ldate | log.Lmicroseconds)
-
-	printWelcome()
-
-	args := os.Args
-	argc := len(os.Args)
-	if argc <= 2 {
-		printHelp()
-		os.Exit(0)
-	}
-
-	//TODO:support UDP protocol
-
-	/*var logFileError error
-	if argc > 5 && args[4] == "-log" {
-		logPath := args[5] + "/" + time.Now().Format("2006_01_02_15_04_05") // "2006-01-02 15:04:05"
-		logPath += args[1] + "-" + strings.Replace(args[2], ":", "_", -1) + "-" + args[3] + ".log"
-		logPath = strings.Replace(logPath, `\`, "/", -1)
-		logPath = strings.Replace(logPath, "//", "/", -1)
-		logFile, logFileError = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE, 0666)
-		if logFileError != nil {
-			log.Fatalln("[x]", "log file path error.", logFileError.Error())
-		}
-		log.Println("[√]", "open test log file success. path:", logPath)
-	}*/
-
-	switch args[1] {
-	case "-listen":
-		if argc < 3 {
-			log.Fatalln(`-listen need two arguments, like "nb -listen 1997 2017".`)
-		}
-		port1 := checkPort(args[2])
-		port2 := checkPort(args[3])
-		log.Println("[√]", "start to listen port:", port1, "and port:", port2)
-		port2port(port1, port2)
-		break
-	case "-tran":
-		if argc < 3 {
-			log.Fatalln(`-tran need two arguments, like "nb -tran 1997 192.168.1.2:3389".`)
-		}
-		port := checkPort(args[2])
-		var remoteAddress string
-		if checkIp(args[3]) {
-			remoteAddress = args[3]
-		}
-		split := strings.SplitN(remoteAddress, ":", 2)
-		log.Println("[√]", "start to transmit address:", remoteAddress, "to address:", split[0]+":"+port)
-		port2host(port, remoteAddress)
-		break
-	case "-slave":
-		if argc < 3 {
-			log.Fatalln(`-slave need two arguments, like "nb -slave 127.0.0.1:3389 8.8.8.8:1997".`)
-		}
-		var address1, address2 string
-		checkIp(args[2])
-		if checkIp(args[2]) {
-			address1 = args[2]
-		}
-		checkIp(args[3])
-		if checkIp(args[3]) {
-			address2 = args[3]
-		}
-		log.Println("[√]", "start to connect address:", address1, "and address:", address2)
-		host2host(address1, address2)
-		break
-	default:
-		printHelp()
-	}
-}
-
-func printWelcome() {
-	fmt.Println("+----------------------------------------------------------------+")
-	fmt.Println("| Welcome to use NATBypass Ver1.0.0 .                            |")
-	fmt.Println("| Code by cw1997 at 2017-10-19 03:59:51                          |")
-	fmt.Println("| If you have some problem when you use the tool,                |")
-	fmt.Println("| please submit issue at : https://github.com/cw1997/NATBypass . |")
-	fmt.Println("+----------------------------------------------------------------+")
-	fmt.Println()
-	// sleep one second because the fmt is not thread-safety.
-	// if not to do this, fmt.Print will print after the log.Print.
-	time.Sleep(time.Second)
-}
-func printHelp() {
-	fmt.Println(`usage: "-listen port1 port2" example: "nb -listen 1997 2017" `)
-	fmt.Println(`       "-tran port1 ip:port2" example: "nb -tran 1997 192.168.1.2:3389" `)
-	fmt.Println(`       "-slave ip1:port1 ip2:port2" example: "nb -slave 127.0.0.1:3389 8.8.8.8:1997" `)
-	fmt.Println(`============================================================`)
-	fmt.Println(`optional argument: "-log logpath" . example: "nb -listen 1997 2017 -log d:/nb" `)
-	fmt.Println(`log filename format: Y_m_d_H_i_s-agrs1-args2-args3.log`)
-	fmt.Println(`============================================================`)
-	fmt.Println(`if you want more help, please read "README.md". `)
-}
-
-func checkPort(port string) string {
-	PortNum, err := strconv.Atoi(port)
-	if err != nil {
-		log.Fatalln("[x]", "port should be a number")
-	}
-	if PortNum < 1 || PortNum > 65535 {
-		log.Fatalln("[x]", "port should be a number and the range is [1,65536)")
-	}
-	return port
-}
-
-func checkIp(address string) bool {
-	ipAndPort := strings.Split(address, ":")
-	if len(ipAndPort) != 2 {
-		log.Fatalln("[x]", "address error. should be a string like [ip:port]. ")
-	}
-	ip := ipAndPort[0]
-	port := ipAndPort[1]
-	checkPort(port)
-	pattern := `^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$`
-	ok, err := regexp.MatchString(pattern, ip)
-	if err != nil || !ok {
-		log.Fatalln("[x]", "ip error. ")
-	}
-	return ok
-}
-
-func port2port(port1 string, port2 string) {
-	listen1 := start_server("0.0.0.0:" + port1)
-	listen2 := start_server("0.0.0.0:" + port2)
-	log.Println("[√]", "listen port:", port1, "and", port2, "success. waiting for client...")
-	for {
-		conn1 := accept(listen1)
-		conn2 := accept(listen2)
-		if conn1 == nil || conn2 == nil {
-			log.Println("[x]", "accept client faild. retry in ", timeout, " seconds. ")
-			time.Sleep(timeout * time.Second)
-			continue
-		}
-		forward(conn1, conn2)
-	}
-}
-
-func port2host(allowPort string, targetAddress string) {
-	server := start_server("0.0.0.0:" + allowPort)
-	for {
-		conn := accept(server)
-		if conn == nil {
-			continue
-		}
-		//println(targetAddress)
-		go func(targetAddress string) {
-			log.Println("[+]", "start connect host:["+targetAddress+"]")
-			target, err := net.Dial("tcp", targetAddress)
-			if err != nil {
-				// temporarily unavailable, don't use fatal.
-				log.Println("[x]", "connect target address ["+targetAddress+"] faild. retry in ", timeout, "seconds. ")
-				conn.Close()
-				log.Println("[←]", "close the connect at local:["+conn.LocalAddr().String()+"] and remote:["+conn.RemoteAddr().String()+"]")
-				time.Sleep(timeout * time.Second)
-				return
-			}
-			log.Println("[→]", "connect target address ["+targetAddress+"] success.")
-			forward(target, conn)
-		}(targetAddress)
-	}
-}
-
-func host2host(address1, address2 string) {
-	for {
-		log.Println("[+]", "try to connect host:["+address1+"] and ["+address2+"]")
-		var host1, host2 net.Conn
-		var err error
-		for {
-			host1, err = net.Dial("tcp", address1)
-			if err == nil {
-				log.Println("[→]", "connect ["+address1+"] success.")
-				break
-			} else {
-				log.Println("[x]", "connect target address ["+address1+"] faild. retry in ", timeout, " seconds. ")
-				time.Sleep(timeout * time.Second)
-			}
-		}
-		for {
-			host2, err = net.Dial("tcp", address2)
-			if err == nil {
-				log.Println("[→]", "connect ["+address2+"] success.")
-				break
-			} else {
-				log.Println("[x]", "connect target address ["+address2+"] faild. retry in ", timeout, " seconds. ")
-				time.Sleep(timeout * time.Second)
-			}
-		}
-		forward(host1, host2)
-	}
-}
-
-func start_server(address string) net.Listener {
-	log.Println("[+]", "try to start server on:["+address+"]")
-	server, err := net.Listen("tcp", address)
-	if err != nil {
-		log.Fatalln("[x]", "listen address ["+address+"] faild.")
-	}
-	log.Println("[√]", "start listen at address:["+address+"]")
-	return server
-	/*defer server.Close()
-
-	for {
-		conn, err := server.Accept()
-		log.Println("accept a new client. remote address:[" + conn.RemoteAddr().String() +
-			"], local address:[" + conn.LocalAddr().String() + "]")
-		if err != nil {
-			log.Println("accept a new client faild.", err.Error())
-			continue
-		}
-		//go recvConnMsg(conn)
-	}*/
-}
-
-func accept(listener net.Listener) net.Conn {
-	conn, err := listener.Accept()
-	if err != nil {
-		log.Println("[x]", "accept connect ["+conn.RemoteAddr().String()+"] faild.", err.Error())
-		return nil
-	}
-	log.Println("[√]", "accept a new client. remote address:["+conn.RemoteAddr().String()+"], local address:["+conn.LocalAddr().String()+"]")
-	return conn
-}
-
-func forward(conn1 net.Conn, conn2 net.Conn) {
-	log.Printf("[+] start transmit. [%s],[%s] <-> [%s],[%s] \n", conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
-	var wg sync.WaitGroup
-	// wait tow goroutines
-	wg.Add(2)
-	go connCopy(conn1, conn2, &wg)
-	go connCopy(conn2, conn1, &wg)
-	//blocking when the wg is locked
-	wg.Wait()
-}
-
-func connCopy(conn1 net.Conn, conn2 net.Conn, wg *sync.WaitGroup) {
-	//TODO:log, record the data from conn1 and conn2.
-	logFile := openLog(conn1.LocalAddr().String(), conn1.RemoteAddr().String(), conn2.LocalAddr().String(), conn2.RemoteAddr().String())
-	if logFile != nil {
-		w := io.MultiWriter(conn1, logFile)
-		io.Copy(w, conn2)
-	} else {
-		io.Copy(conn1, conn2)
-	}
-	conn1.Close()
-	log.Println("[←]", "close the connect at local:["+conn1.LocalAddr().String()+"] and remote:["+conn1.RemoteAddr().String()+"]")
-	//conn2.Close()
-	//log.Println("[←]", "close the connect at local:["+conn2.LocalAddr().String()+"] and remote:["+conn2.RemoteAddr().String()+"]")
-	wg.Done()
-}
-func openLog(address1, address2, address3, address4 string) *os.File {
-	args := os.Args
-	argc := len(os.Args)
-	var logFileError error
-	var logFile *os.File
-	if argc > 5 && args[4] == "-log" {
-		address1 = strings.Replace(address1, ":", "_", -1)
-		address2 = strings.Replace(address2, ":", "_", -1)
-		address3 = strings.Replace(address3, ":", "_", -1)
-		address4 = strings.Replace(address4, ":", "_", -1)
-		timeStr := time.Now().Format("2006_01_02_15_04_05") // "2006-01-02 15:04:05"
-		logPath := args[5] + "/" + timeStr + args[1] + "-" + address1 + "_" + address2 + "-" + address3 + "_" + address4 + ".log"
-		logPath = strings.Replace(logPath, `\`, "/", -1)
-		logPath = strings.Replace(logPath, "//", "/", -1)
-		logFile, logFileError = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE, 0666)
-		if logFileError != nil {
-			log.Fatalln("[x]", "log file path error.", logFileError.Error())
-		}
-		log.Println("[√]", "open test log file success. path:", logPath)
-	}
-	return logFile
-}

+ 2 - 2
mod/utils/utils.go

@@ -68,9 +68,9 @@ func PostBool(r *http.Request, key string) (bool, error) {
 
 	x = strings.TrimSpace(x)
 
-	if x == "1" || strings.ToLower(x) == "true" {
+	if x == "1" || strings.ToLower(x) == "true" || strings.ToLower(x) == "on" {
 		return true, nil
-	} else if x == "0" || strings.ToLower(x) == "false" {
+	} else if x == "0" || strings.ToLower(x) == "false" || strings.ToLower(x) == "off" {
 		return false, nil
 	}
 

+ 2 - 2
start.go

@@ -23,7 +23,7 @@ import (
 	"imuslab.com/zoraxy/mod/sshprox"
 	"imuslab.com/zoraxy/mod/statistic"
 	"imuslab.com/zoraxy/mod/statistic/analytic"
-	"imuslab.com/zoraxy/mod/tcpprox"
+	"imuslab.com/zoraxy/mod/streamproxy"
 	"imuslab.com/zoraxy/mod/tlscert"
 	"imuslab.com/zoraxy/mod/webserv"
 )
@@ -229,7 +229,7 @@ func startupSequence() {
 	webSshManager = sshprox.NewSSHProxyManager()
 
 	//Create TCP Proxy Manager
-	tcpProxyManager = tcpprox.NewTCProxy(&tcpprox.Options{
+	streamProxyManager = streamproxy.NewStreamProxy(&streamproxy.Options{
 		Database:             sysdb,
 		AccessControlHandler: accessController.DefaultAccessRule.AllowConnectionAccess,
 	})

+ 110 - 90
web/components/streamprox.html

@@ -29,9 +29,9 @@
     </div>
     <div class="ui divider"></div>
     <div class="ui basic segment" id="addproxyConfig">
-        <h4>Add or Edit TCP Proxy</h4>
-        <p>Create or edit a new proxy instance</p>
-        <form id="tcpProxyForm" class="ui form">
+        <h4>Add or Edit Stream Proxy</h4>
+        <p>Create or edit a new stream proxy instance</p>
+        <form id="streamProxyForm" class="ui form">
             <div class="field" style="display:none;">
                 <label>UUID</label>
                 <input type="text" name="uuid">
@@ -41,29 +41,41 @@
                 <input type="text" name="name" placeholder="Config Name">
             </div>
             <div class="field">
-                <label>Port A</label>
-                <input type="text" name="porta" placeholder="First address or port">
+                <label>Listening Port / Address with Port</label>
+                <input type="text" name="listenAddr" placeholder="">
+                <small>Port to listen on this host. e.g. :25565 or 127.0.0.1:25565. <br>
+                    If you are using Docker, you will need to expose this port to host network.</small>
             </div>
-                <div class="field">
-                <label>Port B</label>
-            <input type="text" name="portb" placeholder="Second address or port">
+            <div class="field">
+                <label>Proxy Target Address with Port</label>
+                <input type="text" name="proxyAddr" placeholder="">
+                <small>Server address to forward TCP / UDP package. e.g. 192.168.1.100:25565</small>
             </div>
             <div class="field">
                 <label>Timeout (s)</label>
-                <input type="text" name="timeout" placeholder="Timeout (s)">
+                <input type="text" name="timeout" placeholder="" value="10">
+                <small>Connection timeout in seconds</small>
+            </div>
+            <Br>
+            <div class="field">
+                <div class="ui toggle checkbox">
+                    <input type="checkbox" tabindex="0" name="useTCP" class="hidden">
+                    <label>Enable TCP<br>
+                        <small>Forward TCP request on this listening socket</small>
+                    </label>
+                </div>
             </div>
             <div class="field">
-                <label>Mode</label>
-                <select name="mode" class="ui dropdown">
-                    <option value="">Select Mode</option>
-                    <option value="listen">Listen</option>
-                    <option value="transport">Transport</option>
-                    <option value="starter">Starter</option>
-                </select>
+                <div class="ui toggle checkbox">
+                    <input type="checkbox" tabindex="0" name="useUDP" class="hidden">
+                    <label>Enable UDP<br>
+                    <small>Forward UDP request on this listening socket</small></label>
+                </div>
             </div>
-            <button id="addTcpProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>  
-            <button id="editTcpProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);" style="display:none;"><i class="ui green check icon"></i> Update</button>  
-            <button class="ui basic red button" onclick="event.preventDefault(); cancelTCPProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>  
+            <button id="addStreamProxyButton" class="ui basic button" type="submit"><i class="ui green add icon"></i> Create</button>  
+            <button id="editStreamProxyButton" class="ui basic button" onclick="confirmEditTCPProxyConfig(event);" style="display:none;"><i class="ui green check icon"></i> Update</button>  
+            <button class="ui basic red button" onclick="event.preventDefault(); cancelStreamProxyEdit(event);"><i class="ui red remove icon"></i> Cancel</button>  
+            <!-- 
             <div class="ui basic inverted segment" style="background: var(--theme_background_inverted); border-radius: 0.6em;">
                 <p>TCP Proxy support the following TCP sockets proxy modes</p>
                 <table class="ui celled padded inverted basic table">
@@ -128,18 +140,19 @@
                     </tbody>
                   </table>
                 </div>
+                -->
         </form>
     </div>
 </div>
     <script>
-        let editingTCPProxyConfigUUID = ""; //The current editing TCP Proxy config UUID
+        let editingStreamProxyConfigUUID = ""; //The current editing TCP Proxy config UUID
 
-        $("#tcpProxyForm .dropdown").dropdown();
-        $('#tcpProxyForm').on('submit', function(event) {
+        $("#streamProxyForm .dropdown").dropdown();
+        $('#streamProxyForm').on('submit', function(event) {
             event.preventDefault();
 
             //Check if update mode
-            if ($("#editTcpProxyButton").is(":visible")){
+            if ($("#editStreamProxyButton").is(":visible")){
                 confirmEditTCPProxyConfig(event);
                 return;
             }
@@ -154,7 +167,7 @@
             // Send the AJAX POST request
             $.ajax({
                 type: 'POST',
-                url: '/api/tcpprox/config/add',
+                url: '/api/streamprox/config/add',
                 data: form.serialize(),
                 success: function(response) {
                     if (response.error) {
@@ -162,7 +175,7 @@
                     }else{
                         msgbox("Config Added");
                     }
-                    clearTCPProxyAddEditForm();
+                    clearStreamProxyAddEditForm();
                     initProxyConfigList();
                     $("#addproxyConfig").slideUp("fast");
                 },
@@ -172,15 +185,15 @@
             });
         });
             
-        function clearTCPProxyAddEditForm(){
-            $('#tcpProxyForm input, #tcpProxyForm select').val('');
-            $('#tcpProxyForm select').dropdown('clear');
+        function clearStreamProxyAddEditForm(){
+            $('#streamProxyForm input, #streamProxyForm select').val('');
+            $('#streamProxyForm select').dropdown('clear');
         }
 
-        function cancelTCPProxyEdit(event=undefined) {
-            clearTCPProxyAddEditForm();
-            $("#addTcpProxyButton").show();
-            $("#editTcpProxyButton").hide();
+        function cancelStreamProxyEdit(event=undefined) {
+            clearStreamProxyAddEditForm();
+            $("#addStreamProxyButton").show();
+            $("#editStreamProxyButton").hide();
         }
 
         function validateTCPProxyConfig(form){
@@ -230,35 +243,36 @@
                 proxyConfigs.forEach(function(config) {
                     var runningLogo = 'Stopped';
                     var runningClass = "stopped";
-                    var startButton = `<button onclick="startTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="green play icon"></i> Start Proxy</button>`;
+                    var startButton = `<button onclick="startStreamProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="green play icon"></i> Start Proxy</button>`;
                     if (config.Running){
                         runningLogo = 'Running';
-                        startButton = `<button onclick="stopTcpProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`;
+                        startButton = `<button onclick="stopStreamProx('${config.UUID}');" class="ui button" title="Start Proxy"><i class="red stop icon"></i> Stop Proxy</button>`;
                         runningClass = "running"
                     }
 
-                    var modeText = "Unknown";
-                    if (config.Mode == 0){
-                        modeText = "Listen";
-                    }else if (config.Mode == 1){
-                        modeText = "Transport";
-                    }else if (config.Mode == 2){
-                        modeText = "Starter";
+                    var modeText = [];
+                    if (config.UseTCP){
+                        modeText.push("TCP")
+                    }
+
+                    if (config.UseUDP){
+                        modeText.push("UDP")
                     }
 
+                    modeText = modeText.join(" | ")
+
                     var thisConfig = encodeURIComponent(JSON.stringify(config));
 
                     var row = $(`<tr class="tcproxConfig ${runningClass}" uuid="${config.UUID}" config="${thisConfig}">`);
                     row.append($('<td>').html(`
                         ${config.Name}
                         <div class="statusText">${runningLogo}</div>`));
-                    row.append($('<td>').text(config.PortA));
-                    row.append($('<td>').text(config.PortB));
+                    row.append($('<td>').text(config.ListeningAddress));
+                    row.append($('<td>').text(config.ProxyTargetAddr));
                     row.append($('<td>').text(modeText));
                     row.append($('<td>').text(config.Timeout));
                     row.append($('<td>').html(`
                         <div class="ui basic vertical fluid tiny buttons">
-                            <button class="ui button" onclick="validateProxyConfig('${config.UUID}', this);" title="Validate Config"><i class="teal question circle outline icon"></i> CXN Test</button>
                             ${startButton}
                             <button onclick="editTCPProxyConfig('${config.UUID}');" class="ui button" title="Edit Config"><i class="edit icon"></i> Edit </button>
                             <button onclick="deleteTCPProxyConfig('${config.UUID}');" class="ui red basic button" title="Delete Config"><i class="trash icon"></i> Remove</button>
@@ -281,49 +295,46 @@
             return thisConfig;
         }   
 
-        function validateProxyConfig(configUUID, btn){
-            $(btn).html(`<i class="ui loading spinner icon"></i>`);
-            $.ajax({
-                url: "/api/tcpprox/config/validate",
-                data: {uuid: configUUID},
-                success: function(data){
-                    if (data.error != undefined){
-                        let errormsg = data.error.charAt(0).toUpperCase() + data.error.slice(1);
-                        $(btn).html(`<i class="red times icon"></i> ${errormsg}`);
-                        msgbox(data.error, false, 6000);
-                    }else{
-                        $(btn).html(`<i class="green check icon"></i> Config Valid`);
-                        msgbox("Config Check Passed");
-                    }
-                }
-            })   
-        }
-
         function editTCPProxyConfig(configUUID){
             let targetConfig = getConfigDetailsFromDOM(configUUID);
             if (targetConfig != null){
-                $("#addTcpProxyButton").hide();
-                $("#editTcpProxyButton").show();
+                $("#addStreamProxyButton").hide();
+                $("#editStreamProxyButton").show();
                $.each(targetConfig, function(key, value) {
-                    var field = $("#tcpProxyForm").find('[name="' + key.toLowerCase() + '"]');
-                    if (field.length > 0) {
-                        if (field.is('input')) {
-                            field.val(value);
-                        }else if (field.is('select')){
-                            if (key.toLowerCase() == "mode"){
-                                if (value == 0){
-                                    value = "listen";
-                                }else if (value == 1){
-                                    value = "transport";
-                                }else if (value == 2){
-                                    value = "starter";
-                                }
-                            }
-                            $(field).dropdown("set selected", value);
+                    var field;
+                    if (key == "UseTCP"){
+                        let checkboxEle = $("#streamProxyForm input[name=useTCP]").parent();
+                        if (value === true){
+                            $(checkboxEle).checkbox("set checked");
+                        }else{
+                            $(checkboxEle).checkbox("set unchecked");
                         }
+                        return;
+                    }else if (key == "UseUDP"){
+                        let checkboxEle = $("#streamProxyForm input[name=useUDP]").parent();
+                        if (value === true){
+                            $(checkboxEle).checkbox("set checked");
+                        }else{
+                            $(checkboxEle).checkbox("set unchecked");
+                        }
+                        return;
+                    }else if (key == "ListeningAddress"){
+                        field = $("#streamProxyForm input[name=listenAddr]");
+                    }else if (key == "ProxyTargetAddr"){
+                        field = $("#streamProxyForm input[name=proxyAddr]");
+                    }else if (key == "UUID"){
+                        field = $("#streamProxyForm input[name=uuid]");
+                    }else if (key == "Name"){
+                        field = $("#streamProxyForm input[name=name]");
+                    }else if (key == "Timeout"){
+                        field = $("#streamProxyForm input[name=timeout]");
+                    }
+
+                    if (field != undefined && field.length > 0) {
+                        field.val(value);  
                     }
                 });
-                editingTCPProxyConfigUUID = configUUID;
+                editingStreamProxyConfigUUID = configUUID;
                 $("#addproxyConfig").slideDown("fast");
 
             }else{
@@ -334,7 +345,7 @@
         function confirmEditTCPProxyConfig(event){
             event.preventDefault();
             event.stopImmediatePropagation();
-            var form = $("#tcpProxyForm");
+            var form = $("#streamProxyForm");
 
             var formValid = validateTCPProxyConfig(form);
             if (!formValid){
@@ -344,8 +355,17 @@
             // Send the AJAX POST request
             $.ajax({
                 type: 'POST',
-                url: '/api/tcpprox/config/edit',
-                data: form.serialize(),
+                url: '/api/streamprox/config/edit',
+                method: "POST",
+                data: {
+                    uuid: $("#streamProxyForm input[name=uuid]").val().trim(),
+                    name: $("#streamProxyForm input[name=name]").val().trim(),
+                    listenAddr: $("#streamProxyForm input[name=listenAddr]").val().trim(),
+                    proxyAddr: $("#streamProxyForm input[name=proxyAddr]").val().trim(),
+                    useTCP: $("#streamProxyForm input[name=useTCP]")[0].checked ,
+                    useUDP: $("#streamProxyForm input[name=useUDP]")[0].checked ,
+                    timeout: parseInt($("#streamProxyForm input[name=timeout]").val().trim()),
+                },
                 success: function(response) {
                     if (response.error) {
                         msgbox(response.error, false, 6000);
@@ -353,7 +373,7 @@
                         msgbox("Config Updated");
                     }
                     initProxyConfigList();
-                    cancelTCPProxyEdit();
+                    cancelStreamProxyEdit();
                     
                 },
                 error: function() {
@@ -364,7 +384,7 @@
 
         function deleteTCPProxyConfig(configUUID){
             $.ajax({
-                url: "/api/tcpprox/config/delete",
+                url: "/api/streamprox/config/delete",
                 method: "POST",
                 data: {uuid: configUUID},
                 success: function(data){
@@ -379,9 +399,9 @@
         }
 
         //Start a TCP proxy by their config UUID
-        function startTcpProx(configUUID){
+        function startStreamProx(configUUID){
             $.ajax({
-                url: "/api/tcpprox/config/start",
+                url: "/api/streamprox/config/start",
                 method: "POST",
                 data: {uuid: configUUID},
                 success: function(data){
@@ -397,9 +417,9 @@
         }
 
         //Stop a TCP proxy by their config UUID
-        function stopTcpProx(configUUID){
+        function stopStreamProx(configUUID){
             $.ajax({
-                url: "/api/tcpprox/config/stop",
+                url: "/api/streamprox/config/stop",
                 method: "POST",
                 data: {uuid: configUUID},
                 success: function(data){
@@ -417,7 +437,7 @@
         function initProxyConfigList(){
             $.ajax({
                 type: 'GET',
-                url: '/api/tcpprox/config/list',
+                url: '/api/streamprox/config/list',
                 success: function(response) {
                     renderProxyConfigs(response);
                 },