diff --git a/README.md b/README.md index 18ffa51..02af9d2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,196 @@ -# proxylite -A dynamic reverse proxy Golang package for NAT or firewall traversal. Let's build your own tunnel with several lines of code. +[English](./README.md)|[中文](./README_ZH.md) +## proxylite +`proxylite` is a dynamic TCP reverse proxy Golang package for NAT or firewall traversal. It is god damn easy to integrate network traversal feature into your project with proxylite. -动态的Golang反向代理库,用于内网穿透或防火墙穿透。几行代码就能构建你自己的通道。 +Different from well-known [frp](https://github.com/fatedier/frp), `proxylite` is not a set of runnable programs, but a lightweight package, which provides good flexibility and is easy to integrate in golang projects. Of course, build a set of runnable programs is also a piece of cake. + +## Why proxylite + +One day, I needed to add TCP NAT traversal to [my project](https://github.com/piaodazhu/Octopoda), so I tried to introduce [frp](https://github.com/fatedier/frp) to implement this feature. But I realized that frp needs to read configuration files and start processes, which is not a good fit for my needs: +- First of all, starting a process introduces a new independent service to the project, which makes it more difficult to **maintain**. +- Also, this approach does not allow for **easy collection of logging information**. +- Most importantly, I needed to **dynamically establish on-demand connections**, and editing the configuration file and then restarting the process was clearly inelegant. + +So why not write a package to make this more elegant? proxylite was born. Its main features are listed below: +1. Easy to integrate into code. Both server and client structures are provided. Just import this package then register tunnels whenever you want. +2. Dynamic on-demand reverse proxy. **One registration, one port, one user, one tcp connection.** +3. Service registration and discovery. +4. Customized hooks are support. + +## Concepts +``` + +---------------------------------+ +-----------------------------------+ + | Inner Service <--- Inner Client +---+> Listenning Port <--- Outer Port <+---- User + +---------------------------------+ +-----------------------------------+ + NAT Nodes ... Public Server Any User +``` + +## Quick Start + +First, you should import proxylite: +```golang +import "github.com/piaodazhu/proxylite" +``` + +Let's create a server: +```golang +package main +import "github.com/piaodazhu/proxylite" + +func main() { + server := proxylite.NewProxyLiteServer() + server.AddPort(9930, 9932) + panic(server.Run(":9933")) +} +``` + +These code create a proxylite server, and add avaliable outer port 9930-9932 (Note that it is not 9930 and 9932, but from 9930 to 9932), then run the server. The server is blocked listening on port 9939, inner client will dial this port and server discovery also bind this port. + +Then, we create a inner client: +```golang +package main + +import ( + "log" + + "github.com/piaodazhu/proxylite" +) + +func main() { + client := proxylite.NewProxyLiteClient("0.0.0.0:9933") + err := client.RegisterInnerService( + proxylite.RegisterInfo{ + OuterPort: 9931, + InnerAddr: ":22", + Name: "ssh", + Message: "ssh login", + }, + ) + if err != nil { + log.Fatal(err) + return + } + + entry, ok := client.GetRegisterEntryByName("ssh") + if !ok { + log.Fatal("registration failed") + return + } + <-entry.Done + log.Print("BYE :)") +} +``` + +These code create a inner client, binding with server `"0.0.0.0:9933"`. Then we register a inner service to the server: +```golang +proxylite.RegisterInfo{ + OuterPort: 9931, // means we want map server's 9931 port to our inner service + InnerAddr: ":22", // means inner service is 127.0.0.1:22. e.g. default ssh port. + Name: "ssh", // service name + Message: "ssh login", // customized information +}, +``` +Then we get the registration entry if the registration is success. Finally we wait it done by reading channel. + +## Tutorial + +### Server +```golang +func NewProxyLiteServer(portIntervals ...[2]int) *ProxyLiteServer +``` +Create a Proxy server with avaliable ports intervals. + +```golang +func (s *ProxyLiteServer) AddPort(from, to int) bool +``` +Create a Proxy server with avaliable ports intervals. Return false if port is invalid. + +```golang +func (s *ProxyLiteServer) SetLogger(logger *log.Logger) +``` +Set customized logrus logger the the server. + +```golang +func (s *ProxyLiteServer) Run(addr string) error +``` +Run the server and let it listen on given address. + +### client + +```golang +func NewProxyLiteClient(serverAddr string) *ProxyLiteClient +``` +Create a inner client binding with a proxy server. + +```golang +func (c *ProxyLiteClient) AvaliablePorts() ([]int, bool) +``` +Get avaliable ports from proxy server. + +```golang +func (c *ProxyLiteClient) AnyPort() (int, bool) +``` +Get a random avaliable port from proxy server. + +```golang +type ServiceInfo struct { + Port int + Name string + Message string + Busy bool + Birth time.Time +} + +func (c *ProxyLiteClient) ActiveServices() ([]ServiceInfo, error) +``` +Discover all active services from proxy server. + + +```golang +type RegisterInfo struct { + OuterPort int + InnerAddr string + Name string + Message string +} + +func (c *ProxyLiteClient) RegisterInnerService(info RegisterInfo) error +``` +Register inner server to proxy server's outer port. + +```golang +type RegisterEntry struct { + // Basic Info + Info RegisterInfo + // Cancel function + Cancel func() + // Done channel + Done <-chan struct{} +} + +func (c *ProxyLiteClient) GetRegisterEntryByName(name string) (*RegisterEntry, bool) +func (c *ProxyLiteClient) GetRegisterEntryByPort(port int) (*RegisterEntry, bool) +``` +Get RegisterEntry by name or port. RegisterEntry can be used to canncel tunnel or wait done. + +```golang +func (c *ProxyLiteClient) SetLogger(logger *log.Logger) +``` +Set customized logrus logger for the inner client. + +### Others + +```golang +func AskFreePort(addr string) ([]int, error) +``` +Ask avaliable free port from proxy server with given address. + + +```golang +func DiscoverServices(addr string) ([]ServiceInfo, error) +``` +Discover all active services from proxy server with given address. + +## Contributing + +Feel free to open issues or pull requests to make this project better. 🌈 diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..84810eb --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,199 @@ +[English](./README.md)|[中文](./README_ZH.md) + +---- 以下中文文档由ChatGPT生成 ---- + +## proxylite +`proxylite` 是一个用于实现 NAT 或防火墙穿越的动态 TCP 反向代理 Golang 包。使用 `proxylite`,您可以轻松将网络穿越功能集成到您的项目中。 + +与众所周知的 [frp](https://github.com/fatedier/frp) 不同,`proxylite` 不是一组可运行的程序,而是一个轻量级的包,提供了良好的灵活性,并且很容易集成到 Golang 项目中。当然,构建一组可运行的程序也同样简单。 + +## 为什么选择 proxylite + +有一天,我需要为[我的项目](https://github.com/piaodazhu/Octopoda)添加 TCP NAT 穿越功能,所以我尝试引入 [frp](https://github.com/fatedier/frp) 来实现这个功能。但我意识到,frp 需要读取配置文件并启动进程,这不适用于我的需求: +- 首先,启动一个进程会向项目引入一个独立的服务,使得**维护**变得更加困难。 +- 此外,这种方法不允许**轻松收集日志信息**。 +- 最重要的是,我需要**动态地建立按需连接**,而编辑配置文件,然后重新启动进程显然不够优雅。 + +那为什么不编写一个包来使这变得更加优雅呢?proxylite 应运而生。它的主要特点如下: +1. 易于集成到代码中。提供了服务器和客户端结构。只需导入此包,然后在需要时注册隧道。 +2. 动态按需反向代理。**一次注册,一个端口,一个用户,一个 TCP 连接。** +3. 服务注册和发现。 +4. 支持自定义钩子。 + +## 概念 +``` + +-----------------------+ +-----------------------+ + | 内部服务 <--- 内部客户端 +---+> 监听端口 <--- 外部端口 <+---- 用户 + +-----------------------+ +-----------------------+ + NAT 节点 ... 公共服务器 任何用户 +``` + +## 快速开始 + +首先,您应该导入 proxylite: +```golang +import "github.com/piaodazhu/proxylite" +``` + +让我们创建一个服务器: +```golang +package main +import "github.com/piaodazhu/proxylite" + +func main() { + server := proxylite.NewProxyLiteServer() + server.AddPort(9930, 9932) + panic(server.Run(":9933")) +} +``` + +这些代码创建了一个 proxylite 服务器,并添加了可用的外部端口 9930-9932(注意不是 9930 和 9932,而是从 9930 到 9932),然后运行服务器。服务器会阻塞在端口 9939 上进行监听,内部客户端将拨打该端口,而服务器发现也将绑定到该端口。 + +然后,我们创建一个内部客户端: +```golang +package main + +import ( + "log" + + "github.com/piaodazhu/proxylite" +) + +func main() { + client := proxylite.NewProxyLiteClient("0.0.0.0:9933") + err := client.RegisterInnerService( + proxylite.RegisterInfo{ + OuterPort: 9931, + InnerAddr: ":22", + Name: "ssh", + Message: "ssh 登录", + }, + ) + if err != nil { + log.Fatal(err) + return + } + + entry, ok := client.GetRegisterEntryByName("ssh") + if !ok { + log.Fatal("注册失败") + return + } + <-entry.Done + log.Print("再见 :)") +} +``` + +这些代码创建了一个内部客户端,绑定到服务器 `"0.0.0.0:9933"`。然后,我们将一个内部服务注册到服务器上: +```golang +proxylite.RegisterInfo{ + OuterPort: 9931, // 表示我们要将服务器的 9931 端口映射到我们的内部服务 + InnerAddr: ":22", // 表示内部服务为 127.0.0.1:22,例如默认的 SSH 端口 + Name: "ssh", // 服务名称 + Message: "ssh 登录", // 自定义信息 +}, +``` +然后,如果注册成功,我们获取注册条目。最后,我们通过读取通道等待注册完成。 + +## 教程 + +### 服务器 +```golang +func NewProxyLiteServer(portIntervals ...[2]int) *ProxyLiteServer +``` +使用可用的端口间隔创建代理服务器。 + +```golang +func (s *ProxyLiteServer) AddPort(from, to int) bool +``` +使用可用的端口间隔创建代理服务器。如果端口无效,返回 false。 + +```golang +func (s *ProxyLiteServer) SetLogger(logger *log.Logger) +``` +为服务器设置自定义的 logrus 日志记录器。 + +```golang +func (s *ProxyLiteServer) Run(addr string) error +``` +运行服务器,并使其监听给定的地址。 + +### 客户端 + +```golang +func NewProxyLiteClient(serverAddr string) *ProxyLiteClient +``` +创建一个与代理服务器绑定的内部客户端。 + +```golang +func (c *ProxyLiteClient) AvaliablePorts() ([]int, bool) +``` +从代理服务器获取可用端口。 + +```golang +func (c *ProxyLiteClient) AnyPort() (int, bool) +``` +从代理服务器获取一个随机可用端口。 + +```golang +type ServiceInfo struct { + Port int + Name string + Message string + Busy bool + Birth time.Time +} + +func (c *ProxyLiteClient) ActiveServices() ([]ServiceInfo, error) +``` +从代理服务器发现所有活动服务。 + +```golang +type RegisterInfo struct { + OuterPort int + InnerAddr string + Name string + Message string +} + +func (c *ProxyLiteClient) RegisterInnerService(info RegisterInfo) error +``` +将内 + +部服务器注册到代理服务器的外部端口。 + +```golang +type RegisterEntry struct { + // 基本信息 + Info RegisterInfo + // 取消函数 + Cancel func() + // 完成通道 + Done <-chan struct{} +} + +func (c *ProxyLiteClient) GetRegisterEntryByName(name string) (*RegisterEntry, bool) +func (c *ProxyLiteClient) GetRegisterEntryByPort(port int) (*RegisterEntry, bool) +``` +通过名称或端口获取 RegisterEntry。RegisterEntry 可用于取消隧道或等待完成。 + +```golang +func (c *ProxyLiteClient) SetLogger(logger *log.Logger) +``` +为内部客户端设置自定义的 logrus 日志记录器。 + +### 其他 + +```golang +func AskFreePort(addr string) ([]int, error) +``` +从给定地址的代理服务器请求可用的空闲端口。 + +```golang +func DiscoverServices(addr string) ([]ServiceInfo, error) +``` +从给定地址的代理服务器发现所有活动服务。 + +## 贡献 + +欢迎提出问题或拉取请求,以使这个项目变得更好。 🌈 \ No newline at end of file diff --git a/client.go b/client.go index b3fb96d..0c325ed 100644 --- a/client.go +++ b/client.go @@ -13,9 +13,13 @@ import ( log "github.com/sirupsen/logrus" ) +// RegisterEntry entry to discribe a single service registration type RegisterEntry struct { + // Basic Info Info RegisterInfo + // Cancel function Cancel func() + // Done channel Done <-chan struct{} } @@ -28,6 +32,7 @@ type ProxyLiteClient struct { logger *log.Logger } +// NewProxyLiteClient Create a inner client binding with a proxy server. func NewProxyLiteClient(serverAddr string) *ProxyLiteClient { client := &ProxyLiteClient{ ready: false, @@ -49,6 +54,7 @@ func NewProxyLiteClient(serverAddr string) *ProxyLiteClient { return client } +// AvaliablePorts Get avaliable ports from proxy server. func (c *ProxyLiteClient) AvaliablePorts() ([]int, bool) { ports, err := AskFreePort(c.serverAddr) if err != nil { @@ -62,6 +68,7 @@ func (c *ProxyLiteClient) AvaliablePorts() ([]int, bool) { return ports, true } +// AnyPort Get a random avaliable port from proxy server. func (c *ProxyLiteClient) AnyPort() (int, bool) { if c.ready { for port := range c.avaliablePorts { @@ -81,6 +88,7 @@ func (c *ProxyLiteClient) AnyPort() (int, bool) { return 0, false } +// ActiveServices Discover all active services from proxy server. func (c *ProxyLiteClient) ActiveServices() ([]ServiceInfo, error) { return DiscoverServices(c.serverAddr) } @@ -118,6 +126,12 @@ func register(conn net.Conn, info RegisterInfo) error { return nil } +// SetLogger Set customized logrus logger for the inner client. +func (c *ProxyLiteClient) SetLogger(logger *log.Logger) { + c.logger = logger +} + +// RegisterInnerService Register inner server to proxy server's outer port. func (c *ProxyLiteClient) RegisterInnerService(info RegisterInfo) error { if !c.ready { return errors.New("client not ready") @@ -214,6 +228,7 @@ func (c *ProxyLiteClient) RegisterInnerService(info RegisterInfo) error { return nil } +// GetRegisterEntryByName Get RegisterEntry func (c *ProxyLiteClient) GetRegisterEntryByName(name string) (*RegisterEntry, bool) { c.lock.RLock() defer c.lock.RUnlock() @@ -225,6 +240,7 @@ func (c *ProxyLiteClient) GetRegisterEntryByName(name string) (*RegisterEntry, b return nil, false } +// GetRegisterEntryByPort Get RegisterEntry func (c *ProxyLiteClient) GetRegisterEntryByPort(port int) (*RegisterEntry, bool) { c.lock.RLock() defer c.lock.RUnlock() @@ -232,6 +248,11 @@ func (c *ProxyLiteClient) GetRegisterEntryByPort(port int) (*RegisterEntry, bool return entry, exists } +func (c *ProxyLiteClient) logTunnelMessage(service, header, msg string) { + c.logger.Infof("[%s] [%s] %s", service, header, msg) +} + +// AskFreePort Ask avaliable free port from proxy server with given address. func AskFreePort(addr string) ([]int, error) { conn, err := net.Dial("tcp", addr) if err != nil { @@ -263,6 +284,7 @@ func AskFreePort(addr string) ([]int, error) { return rsp.Ports, nil } +// DiscoverServices Discover all active services from proxy server with given address. func DiscoverServices(addr string) ([]ServiceInfo, error) { conn, err := net.Dial("tcp", addr) if err != nil { @@ -293,10 +315,3 @@ func DiscoverServices(addr string) ([]ServiceInfo, error) { return rsp.Services, nil } -func (c *ProxyLiteClient) SetLogger(logger *log.Logger) { - c.logger = logger -} - -func (c *ProxyLiteClient) logTunnelMessage(service, header, msg string) { - c.logger.Infof("[%s] [%s] %s", service, header, msg) -} diff --git a/client_test.go b/client_test.go index e45b6b0..ae7819f 100644 --- a/client_test.go +++ b/client_test.go @@ -1,41 +1 @@ package proxylite - -import ( - "testing" - "time" -) - -func TestClient(t *testing.T) { - client := NewProxyLiteClient(":9933") - t.Log(client.AvaliablePorts()) - t.Log(client.ActiveServices()) - t.Log(client.AnyPort()) - t.Log(client.AnyPort()) - t.Log(client.AnyPort()) - - t.Log(client.RegisterInnerService( - RegisterInfo{ - OuterPort: 9931, - InnerAddr: ":22", - Name: "ssh", - Message: "ssh login", - }, - )) - - t.Log(client.AvaliablePorts()) - t.Log(client.ActiveServices()) - t.Log(client.GetRegisterEntryByPort(9931)) - t.Log(client.GetRegisterEntryByName("ssh")) - entry, ok := client.GetRegisterEntryByName("ssh") - - if !ok { - t.Error("register failed") - return - } - <-entry.Done - time.Sleep(time.Microsecond * 10) - t.Log(client.AvaliablePorts()) - t.Log(client.ActiveServices()) - t.Log(client.GetRegisterEntryByPort(9931)) - t.Log(client.GetRegisterEntryByName("ssh")) -} diff --git a/example/client/go.mod b/example/client/go.mod index 8f04dce..77cf08e 100644 --- a/example/client/go.mod +++ b/example/client/go.mod @@ -1,3 +1,10 @@ module client go 1.20 + +require github.com/piaodazhu/proxylite v0.0.0-20230813091435-b2c8f474a9fc + +require ( + github.com/sirupsen/logrus v1.9.3 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/example/client/go.sum b/example/client/go.sum new file mode 100644 index 0000000..f4833bc --- /dev/null +++ b/example/client/go.sum @@ -0,0 +1,17 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/piaodazhu/proxylite v0.0.0-20230813091435-b2c8f474a9fc h1:gWwkhe3teNrzCRAwSpOIDfrAdvBEMK+a+t9mgPuNb2o= +github.com/piaodazhu/proxylite v0.0.0-20230813091435-b2c8f474a9fc/go.mod h1:nMXTXWiMgKmhhmV8UaSwIRgoCectdt7p8xp8mB6bUfk= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server.go b/server.go index f8a4685..f9801f9 100644 --- a/server.go +++ b/server.go @@ -27,6 +27,7 @@ type ProxyLiteServer struct { logger *log.Logger } +// NewProxyLiteServer Create a Proxy server with avaliable ports intervals. func NewProxyLiteServer(portIntervals ...[2]int) *ProxyLiteServer { server := &ProxyLiteServer{ all: map[int]struct{}{}, @@ -40,6 +41,7 @@ func NewProxyLiteServer(portIntervals ...[2]int) *ProxyLiteServer { return server } +// AddPort Add avaliable ports intervals for server. Return false if port is invalid. func (s *ProxyLiteServer) AddPort(from, to int) bool { if from <= 0 || to > 65535 { return false @@ -50,6 +52,13 @@ func (s *ProxyLiteServer) AddPort(from, to int) bool { return true } +// SetLogger Set customized logrus logger for the server. +func (s *ProxyLiteServer) SetLogger(logger *log.Logger) { + s.logger = logger +} + + +// Run Run the server and let it listen on given address. func (s *ProxyLiteServer) Run(addr string) error { listener, err := net.Listen("tcp", addr) if err != nil { @@ -370,10 +379,6 @@ func (s *ProxyLiteServer) startTunnel(tn *tunnel) { time.Sleep(time.Millisecond * 10) } -func (s *ProxyLiteServer) SetLogger(logger *log.Logger) { - s.logger = logger -} - func (s *ProxyLiteServer) logProtocolMessage(source, header string, req, rsp interface{}) { s.logger.Infof("[%s] [%s] req=%#v, rsp=%#v", source, header, req, rsp) } diff --git a/server_test.go b/server_test.go index 0f0624b..ae7819f 100644 --- a/server_test.go +++ b/server_test.go @@ -1,35 +1 @@ package proxylite - -import ( - "fmt" - "testing" -) - -func TestServer(t *testing.T) { - server := NewProxyLiteServer() - server.AddPort(9930, 9932) - server.Run(":9933") -} - -func TestRegister(t *testing.T) { - server := NewProxyLiteServer() - server.AddPort(9930, 9932) - server.Run(":9933") -} - -type Student struct { - Name string - Age int -} - -func printInterface(obj interface{}) { - fmt.Printf("%#v", obj) -} - -func TestPrintInterface(t *testing.T) { - stu := &Student{ - Name: "Alice", - Age: 11, - } - printInterface(stu) -}