├── staticcheck.conf ├── go.mod ├── .gitignore ├── configs ├── wsps_template.json └── wspc_template.json ├── pkg ├── msg │ ├── data.go │ ├── version.go │ ├── msg.proto │ ├── config.go │ └── msg.pb.go ├── server │ ├── channel.go │ ├── dynamic.go │ ├── config.go │ ├── router.go │ ├── remote.go │ ├── http.go │ └── server.go ├── client │ ├── config.go │ ├── dynamic.go │ ├── remote.go │ ├── local.go │ ├── http.go │ ├── client.go │ └── socks5.go ├── logger │ ├── config.go │ └── logger.go └── stream │ ├── wrap.go │ ├── pipe.go │ ├── conn.go │ ├── utils.go │ ├── bridge.go │ ├── wan.go │ └── handler.go ├── cmd ├── wspc │ ├── main.go │ └── cmd.go └── wsps │ ├── cmd.go │ └── main.go ├── .github └── workflows │ └── go.yml ├── .goreleaser.yml ├── go.sum ├── wsp_test.go ├── LICENSE └── README.md /staticcheck.conf: -------------------------------------------------------------------------------- 1 | checks = ["all"] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gowsp/wsp 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/gobwas/ws v1.4.0 7 | github.com/segmentio/ksuid v1.0.4 8 | google.golang.org/protobuf v1.34.2 9 | ) 10 | 11 | require ( 12 | github.com/gobwas/httphead v0.1.0 // indirect 13 | github.com/gobwas/pool v0.2.1 // indirect 14 | golang.org/x/sys v0.21.0 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .vscode/ 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | 18 | dist/ 19 | -------------------------------------------------------------------------------- /configs/wsps_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "level": "info", 4 | "output": "/var/log/wsps.log" 5 | }, 6 | "ssl": { 7 | "enable": false, 8 | "key": "/var/cert/key.file", 9 | "cert": "/var/cert/cert.file" 10 | }, 11 | "host": "mywsps.com", 12 | "auth": "auth", 13 | "path": "/proxy", 14 | "port": 8010 15 | } -------------------------------------------------------------------------------- /pkg/msg/data.go: -------------------------------------------------------------------------------- 1 | //Package msg is Data transferred before client and server 2 | package msg 3 | 4 | type Data struct { 5 | Msg *WspMessage 6 | Raw *[]byte 7 | } 8 | 9 | func (data *Data) ID() string { 10 | return data.Msg.Id 11 | } 12 | func (data *Data) Cmd() WspCmd { 13 | return data.Msg.Cmd 14 | } 15 | func (data *Data) Payload() []byte { 16 | return data.Msg.Data 17 | } 18 | -------------------------------------------------------------------------------- /pkg/msg/version.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import "strconv" 4 | 5 | const PROTOCOL_VERSION Protocol = 0.10 6 | 7 | type Protocol float64 8 | 9 | func ParseVersion(version string) (Protocol, error) { 10 | proto, err := strconv.ParseFloat(version, 64) 11 | return Protocol(proto), err 12 | } 13 | func (p Protocol) String() string { 14 | return strconv.FormatFloat(float64(p), 'f', 2, 64) 15 | } 16 | func (p Protocol) Major() int { 17 | return int(p) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/server/channel.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var hub = &Hub{} 8 | 9 | type Hub struct { 10 | listen sync.Map 11 | } 12 | 13 | func (s *Hub) Load(channel string) (interface{}, bool) { 14 | return s.listen.Load(channel) 15 | } 16 | func (s *Hub) Exist(channel string) (exist bool) { 17 | _, exist = s.listen.Load(channel) 18 | return 19 | } 20 | func (s *Hub) Store(channel string, r interface{}) { 21 | s.listen.Store(channel, r) 22 | } 23 | func (s *Hub) Remove(channel string) { 24 | s.listen.Delete(channel) 25 | } 26 | -------------------------------------------------------------------------------- /cmd/wspc/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | 8 | "github.com/gowsp/wsp/pkg/client" 9 | "github.com/gowsp/wsp/pkg/logger" 10 | ) 11 | 12 | func main() { 13 | config, err := parseConfig() 14 | if err != nil { 15 | logger.Error("start wspc error: %s", err) 16 | return 17 | } 18 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 19 | defer stop() 20 | config.Log.Init() 21 | for _, wspc := range config.Client { 22 | go client.New(wspc).ListenAndServe() 23 | } 24 | <-ctx.Done() 25 | logger.Info("wspc closed") 26 | } 27 | -------------------------------------------------------------------------------- /pkg/msg/msg.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "github.com/gowsp/wsp/pkg/msg"; 3 | 4 | enum WspCmd { 5 | CONNECT = 0; 6 | RESPOND = 1; 7 | TRANSFER = 2; 8 | INTERRUPT = 3; 9 | } 10 | message WspMessage { 11 | string id = 1; 12 | WspCmd cmd = 2; 13 | bytes data = 3; 14 | } 15 | 16 | enum WspType { 17 | LOCAL = 0; 18 | REMOTE = 1; 19 | DYNAMIC = 2; 20 | } 21 | message WspRequest { 22 | WspType type = 1; 23 | string data = 2; 24 | } 25 | 26 | enum WspCode { 27 | FAILED = 0; 28 | SUCCESS = 1; 29 | } 30 | message WspResponse { 31 | WspCode code = 1; 32 | string data = 2; 33 | } -------------------------------------------------------------------------------- /configs/wspc_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "level": "info", 4 | "output": "/var/log/wspc.log" 5 | }, 6 | "client": [ 7 | { 8 | "auth": "auth", 9 | "server": "ws://mywsps.com:8010/proxy", 10 | "dynamic": [ 11 | "socks5://:1080" 12 | ], 13 | "remote": [ 14 | "tcp://ssh:passwod@192.168.1.100:22", 15 | "http://127.0.0.1:8080?mode=path&value=api", 16 | "http://127.0.0.1:8080?mode=domain&value=custom.com" 17 | ], 18 | "local": [ 19 | "tcp://ssh:passwod@127.0.0.1:2200" 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - 18 | name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: 1.18 22 | - 23 | name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v6 25 | with: 26 | distribution: goreleaser 27 | version: latest 28 | args: release --clean 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /pkg/server/dynamic.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/gowsp/wsp/pkg/logger" 8 | "github.com/gowsp/wsp/pkg/msg" 9 | "github.com/gowsp/wsp/pkg/stream" 10 | ) 11 | 12 | func (c *conn) NewDynamic(id string, conf *msg.WspConfig) error { 13 | address := conf.Address() 14 | logger.Info("open connect %s", address) 15 | 16 | remote, err := net.DialTimeout(conf.Network(), address, 5*time.Second) 17 | if err != nil { 18 | return err 19 | } 20 | local, err := c.wan.Accept(id, remote.RemoteAddr(), conf) 21 | if err != nil { 22 | return err 23 | } 24 | stream.Copy(local, remote) 25 | logger.Info("close connect %s, addr %s", address, remote.RemoteAddr()) 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/client/config.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gowsp/wsp/pkg/logger" 7 | ) 8 | 9 | type sliceArg []string 10 | 11 | func (i *sliceArg) String() string { 12 | return "slice agrs" 13 | } 14 | 15 | func (i *sliceArg) Set(value string) error { 16 | args := strings.Split(value, " ") 17 | *i = append(*i, args...) 18 | return nil 19 | } 20 | 21 | type Config struct { 22 | Log logger.Config `json:"log,omitempty"` 23 | Client []WspcConfig `json:"client,omitempty"` 24 | } 25 | 26 | type WspcConfig struct { 27 | Auth string `json:"auth,omitempty"` 28 | Server string `json:"server,omitempty"` 29 | Local sliceArg `json:"local,omitempty"` 30 | Remote sliceArg `json:"remote,omitempty"` 31 | Dynamic sliceArg `json:"dynamic,omitempty"` 32 | } 33 | -------------------------------------------------------------------------------- /pkg/server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gowsp/wsp/pkg/logger" 7 | ) 8 | 9 | type Config struct { 10 | Log logger.Config `json:"log,omitempty"` 11 | 12 | SSL SSL `json:"ssl,omitempty"` 13 | Host string `json:"host,omitempty"` 14 | Auth string `json:"auth,omitempty"` 15 | Path string `json:"path,omitempty"` 16 | Port uint64 `json:"port,omitempty"` 17 | } 18 | 19 | type SSL struct { 20 | Enable bool `json:"enable,omitempty"` 21 | Key string `json:"key,omitempty"` 22 | Cert string `json:"cert,omitempty"` 23 | } 24 | 25 | func (c *Config) EnbleSSL() bool { 26 | return c.SSL.Enable && c.SSL.Key != "" && c.SSL.Cert != "" 27 | } 28 | func (c *Config) clean() { 29 | c.Path = strings.TrimPrefix(c.Path, "/") 30 | c.Path = strings.TrimSpace(c.Path) 31 | } 32 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: wsp 2 | before: 3 | hooks: 4 | - go mod tidy 5 | builds: 6 | - main: ./cmd/wspc 7 | id: "wspc" 8 | binary: wspc 9 | goos: 10 | - linux 11 | - darwin 12 | - windows 13 | - main: ./cmd/wsps 14 | id: "wsps" 15 | binary: wsps 16 | goos: 17 | - linux 18 | - darwin 19 | - windows 20 | archives: 21 | - format_overrides: 22 | - goos: windows 23 | format: zip 24 | files: 25 | - LICENSE 26 | - README.md 27 | - configs/* 28 | checksum: 29 | name_template: 'checksums.txt' 30 | snapshot: 31 | name_template: "{{ incpatch .Version }}-next" 32 | changelog: 33 | sort: asc 34 | filters: 35 | exclude: 36 | - '^ci:' 37 | - '^docs:' 38 | - '^test:' 39 | release: 40 | github: 41 | owner: gowsp 42 | name: wsp -------------------------------------------------------------------------------- /pkg/logger/config.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | type Config struct { 11 | Level string `json:"level,omitempty"` 12 | Output string `json:"output,omitempty"` 13 | } 14 | 15 | func (c *Config) Init() { 16 | flag := log.LstdFlags 17 | switch strings.ToUpper(c.Level) { 18 | case "ERROR": 19 | level = error 20 | case "DEBUG": 21 | flag |= log.Lshortfile 22 | level = debug 23 | case "TRACE": 24 | flag |= log.Lshortfile 25 | level = trace 26 | } 27 | out := output(c.Output) 28 | logger = log.New(out, "", flag) 29 | } 30 | 31 | func output(path string) io.Writer { 32 | if path == "" { 33 | return os.Stdout 34 | } 35 | file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0666) 36 | if err != nil { 37 | logger.Fatalln(err) 38 | } 39 | return file 40 | } 41 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | const ( 10 | error = iota 11 | info 12 | debug 13 | trace 14 | ) 15 | 16 | var level int = info 17 | var logger *log.Logger = log.New(os.Stdout, "", log.LstdFlags) 18 | 19 | func Fatalln(v ...any) { 20 | logger.Output(3, fmt.Sprintln(v...)) 21 | os.Exit(1) 22 | } 23 | func Error(format string, v ...any) { 24 | logger.Output(3, fmt.Sprintf("[ERROR] "+format+"\n", v...)) 25 | } 26 | func Info(format string, v ...any) { 27 | if level > error { 28 | logger.Output(3, fmt.Sprintf("[INFO] "+format+"\n", v...)) 29 | } 30 | } 31 | func Debug(format string, v ...any) { 32 | if level > info { 33 | logger.Output(3, fmt.Sprintf("[DEBUG] "+format+"\n", v...)) 34 | } 35 | } 36 | func Trace(format string, v ...any) { 37 | if level > debug { 38 | logger.Output(3, fmt.Sprintf("[TRACE] "+format+"\n", v...)) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/stream/wrap.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | 8 | "github.com/gobwas/ws" 9 | "github.com/gobwas/ws/wsutil" 10 | ) 11 | 12 | func NewReadWriter(conn net.Conn) io.ReadWriteCloser { 13 | return &wsReadWriter{ 14 | conn: conn, 15 | } 16 | } 17 | 18 | type wsReadWriter struct { 19 | lock sync.Mutex 20 | conn net.Conn 21 | } 22 | 23 | func (w *wsReadWriter) Read(p []byte) (n int, err error) { 24 | val, err := wsutil.ReadClientBinary(w.conn) 25 | if err != nil { 26 | return len(val), err 27 | } 28 | copy(p, val) 29 | return len(val), err 30 | } 31 | 32 | func (w *wsReadWriter) Write(p []byte) (n int, err error) { 33 | w.lock.Lock() 34 | defer w.lock.Unlock() 35 | err = wsutil.WriteMessage(w.conn, ws.StateServerSide, ws.OpBinary, p) 36 | if err == nil { 37 | n = len(p) 38 | } 39 | return 40 | } 41 | 42 | func (w *wsReadWriter) Close() error { 43 | return w.conn.Close() 44 | } 45 | -------------------------------------------------------------------------------- /pkg/client/dynamic.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/gowsp/wsp/pkg/logger" 7 | "github.com/gowsp/wsp/pkg/msg" 8 | ) 9 | 10 | type DynamicProxy interface { 11 | // Listen local 12 | Listen() 13 | 14 | // ServeConn conn by wspc 15 | ServeConn(conn net.Conn) error 16 | } 17 | 18 | func (c *Wspc) DynamicForward() { 19 | for _, val := range c.config.Dynamic { 20 | conf, err := msg.NewWspConfig(msg.WspType_DYNAMIC, val) 21 | if err != nil { 22 | logger.Error("dynamic config error: %s", err) 23 | continue 24 | } 25 | c.ListenDynamic(conf) 26 | } 27 | } 28 | 29 | func (c *Wspc) ListenDynamic(conf *msg.WspConfig) { 30 | var proxy DynamicProxy 31 | switch conf.Scheme() { 32 | case "http": 33 | proxy = &HTTPProxy{conf: conf, wspc: c} 34 | case "socks5": 35 | proxy = &Socks5Proxy{conf: conf, wspc: c} 36 | default: 37 | logger.Info("dynamic not supported %s", conf.Scheme()) 38 | return 39 | } 40 | go proxy.Listen() 41 | } 42 | -------------------------------------------------------------------------------- /cmd/wspc/cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/gowsp/wsp/pkg/client" 11 | "github.com/gowsp/wsp/pkg/msg" 12 | ) 13 | 14 | var date string 15 | var version string 16 | 17 | var ( 18 | configFile string 19 | showVersion bool 20 | ) 21 | 22 | func parseConfig() (*client.Config, error) { 23 | flag.BoolVar(&showVersion, "v", false, "print version and exit") 24 | flag.StringVar(&configFile, "c", "wspc.json", "Specifies an alternative per-user configuration file") 25 | flag.Parse() 26 | 27 | if showVersion { 28 | fmt.Printf("Version: %s\n", version) 29 | fmt.Printf("Release Date: %s\n", date) 30 | fmt.Printf("Protocol Version: %s\n", msg.PROTOCOL_VERSION) 31 | os.Exit(0) 32 | } 33 | if configFile == "" { 34 | return nil, errors.New("config file does not exist") 35 | } 36 | conf, err := os.ReadFile(configFile) 37 | if err != nil { 38 | return nil, err 39 | } 40 | config := new(client.Config) 41 | err = json.Unmarshal(conf, config) 42 | return config, err 43 | } 44 | -------------------------------------------------------------------------------- /cmd/wsps/cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/gowsp/wsp/pkg/msg" 11 | "github.com/gowsp/wsp/pkg/server" 12 | ) 13 | 14 | var date string 15 | var version string 16 | 17 | var ( 18 | configFile string 19 | showVersion bool 20 | ) 21 | 22 | func parseConfig() (*server.Config, error) { 23 | flag.BoolVar(&showVersion, "v", false, "print version and exit") 24 | flag.StringVar(&configFile, "c", "wsps.json", "Specifies an alternative per-user configuration file") 25 | flag.Parse() 26 | 27 | if showVersion { 28 | fmt.Printf("Version: %s\n", version) 29 | fmt.Printf("Release Date: %s\n", date) 30 | fmt.Printf("Protocol Version: %s\n", msg.PROTOCOL_VERSION) 31 | os.Exit(0) 32 | } 33 | if configFile == "" { 34 | return nil, errors.New("config file does not exist") 35 | } 36 | conf, err := os.ReadFile(configFile) 37 | if err != nil { 38 | return nil, err 39 | } 40 | config := new(server.Config) 41 | err = json.Unmarshal(conf, config) 42 | return config, err 43 | } 44 | -------------------------------------------------------------------------------- /pkg/client/remote.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | "github.com/gowsp/wsp/pkg/logger" 9 | "github.com/gowsp/wsp/pkg/msg" 10 | "github.com/gowsp/wsp/pkg/stream" 11 | ) 12 | 13 | func (c *Wspc) NewConn(data *msg.Data, req *msg.WspRequest) error { 14 | remote, err := req.ToConfig() 15 | if err != nil { 16 | return err 17 | } 18 | channel := remote.Channel() 19 | logger.Info("received channel %s conn", channel) 20 | conf, err := c.LoadConfig(channel) 21 | if err != nil { 22 | return err 23 | } 24 | if conf.Paasowrd() != remote.Paasowrd() { 25 | return fmt.Errorf("%s password is incorrect", channel) 26 | } 27 | var conn net.Conn 28 | if conf.IsTunnel() { 29 | conn, err = net.DialTimeout(remote.Network(), remote.Address(), 7*time.Second) 30 | } else { 31 | conn, err = net.DialTimeout(conf.Network(), conf.Address(), 7*time.Second) 32 | } 33 | if err != nil { 34 | return err 35 | } 36 | local, err := c.wan.Accept(data.ID(), conn.RemoteAddr(), conf) 37 | if err != nil { 38 | return err 39 | } 40 | stream.Copy(local, conn) 41 | logger.Info("close channel %s conn %s", channel, conn.RemoteAddr()) 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 2 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 3 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 4 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 5 | github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= 6 | github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= 7 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 8 | github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= 9 | github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= 10 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 11 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 12 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 13 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 14 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 15 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 16 | -------------------------------------------------------------------------------- /cmd/wsps/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/gowsp/wsp/pkg/logger" 14 | "github.com/gowsp/wsp/pkg/server" 15 | ) 16 | 17 | func main() { 18 | config, err := parseConfig() 19 | if err != nil { 20 | logger.Error("start wsps error: %s", err) 21 | return 22 | } 23 | config.Log.Init() 24 | wsps := server.New(config) 25 | addr := fmt.Sprintf(":%d", config.Port) 26 | srv := &http.Server{Handler: wsps, Addr: addr} 27 | go func() { 28 | logger.Info("wsps will start at %s, path %s ", addr, config.Path) 29 | if config.EnbleSSL() { 30 | if err := srv.ListenAndServeTLS(config.SSL.Cert, config.SSL.Key); err != nil && errors.Is(err, http.ErrServerClosed) { 31 | logger.Error("%s", err) 32 | } 33 | } else { 34 | if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) { 35 | logger.Error("%s", err) 36 | } 37 | } 38 | }() 39 | 40 | quit := make(chan os.Signal, 1) 41 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 42 | <-quit 43 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 44 | defer cancel() 45 | 46 | if err := srv.Shutdown(ctx); err != nil { 47 | logger.Fatalln("wsps forced to shutdown: %s", err) 48 | } 49 | logger.Info("wsps exiting") 50 | } 51 | -------------------------------------------------------------------------------- /pkg/server/router.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "sync" 7 | 8 | "github.com/gobwas/ws" 9 | "github.com/gowsp/wsp/pkg/logger" 10 | "github.com/gowsp/wsp/pkg/msg" 11 | "github.com/gowsp/wsp/pkg/stream" 12 | ) 13 | 14 | type conn struct { 15 | wsps *Wsps 16 | wan *stream.Wan 17 | http sync.Map 18 | listen sync.Map 19 | } 20 | 21 | func (c *conn) config() *Config { 22 | return c.wsps.config 23 | } 24 | 25 | func (c *conn) close() { 26 | logger.Info("close websocket connect") 27 | c.listen.Range(func(key, value any) bool { 28 | hub.Remove(key.(string)) 29 | return true 30 | }) 31 | } 32 | func (c *conn) ListenAndServe(w net.Conn) { 33 | c.wan = stream.NewWan(w, ws.StateServerSide) 34 | stream.NewHandler(c).Serve(c.wan) 35 | } 36 | 37 | func (c *conn) NewConn(data *msg.Data, req *msg.WspRequest) error { 38 | conf, err := req.ToConfig() 39 | if err != nil { 40 | return err 41 | } 42 | id := data.ID() 43 | switch req.Type { 44 | case msg.WspType_DYNAMIC: 45 | return c.NewDynamic(id, conf) 46 | case msg.WspType_LOCAL: 47 | return c.NewRemoteConn(data, conf) 48 | case msg.WspType_REMOTE: 49 | return c.AddRemote(id, conf) 50 | default: 51 | return fmt.Errorf("unknown %v", req.Type) 52 | } 53 | } 54 | func (c *conn) LoadConfig(channel string) (*msg.WspConfig, error) { 55 | if val, ok := c.listen.Load(channel); ok { 56 | return val.(*msg.WspConfig), nil 57 | } 58 | return nil, fmt.Errorf(channel + " not found") 59 | } 60 | -------------------------------------------------------------------------------- /pkg/client/local.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/gowsp/wsp/pkg/logger" 7 | "github.com/gowsp/wsp/pkg/msg" 8 | "github.com/gowsp/wsp/pkg/stream" 9 | ) 10 | 11 | func (c *Wspc) LocalForward() { 12 | for _, val := range c.config.Local { 13 | conf, err := msg.NewWspConfig(msg.WspType_LOCAL, val) 14 | if err != nil { 15 | logger.Error("local config %s error: %s", val, err) 16 | continue 17 | } 18 | go c.ListenLocal(conf) 19 | } 20 | } 21 | func (c *Wspc) ListenLocal(conf *msg.WspConfig) { 22 | channel := conf.Channel() 23 | logger.Info("listen local on channel %s", channel) 24 | local, err := net.Listen(conf.Network(), conf.Address()) 25 | if err != nil { 26 | logger.Error("listen local channel %s, error: %s", channel, err) 27 | return 28 | } 29 | for { 30 | conn, err := local.Accept() 31 | if err != nil { 32 | logger.Error("accept local channel %s, error: %s", channel, err) 33 | continue 34 | } 35 | go c.NewLocalConn(conn, conf) 36 | } 37 | } 38 | 39 | func (c *Wspc) NewLocalConn(local net.Conn, config *msg.WspConfig) { 40 | channel := config.Channel() 41 | logger.Info("open remote channel %s", channel) 42 | remote, err := c.wan.DialTCP(local.LocalAddr(), config) 43 | if err != nil { 44 | local.Close() 45 | logger.Error("close local channel %s, addr %s, error: %s", channel, local.LocalAddr(), err) 46 | return 47 | } 48 | stream.Copy(local, remote) 49 | logger.Info("close local channel %s, addr %s", channel, local.LocalAddr()) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/stream/pipe.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/gowsp/wsp/pkg/msg" 10 | ) 11 | 12 | type Conn interface { 13 | TaskID() uint64 14 | Rewrite(data *msg.Data) (n int, err error) 15 | Interrupt() error 16 | io.Closer 17 | } 18 | 19 | func newConn(local net.Addr, remote *link) *conn { 20 | pr, pw := io.Pipe() 21 | return &conn{ 22 | taskID: nextTaskID(), 23 | pr: pr, 24 | pw: pw, 25 | local: local, 26 | remote: remote, 27 | } 28 | } 29 | 30 | type conn struct { 31 | taskID uint64 32 | local net.Addr 33 | remote *link 34 | pr *io.PipeReader 35 | pw *io.PipeWriter 36 | close sync.Once 37 | } 38 | 39 | func (c *conn) TaskID() uint64 { 40 | return c.taskID 41 | } 42 | func (c *conn) Rewrite(data *msg.Data) (n int, err error) { 43 | return c.pw.Write(data.Payload()) 44 | } 45 | func (c *conn) Read(p []byte) (n int, err error) { 46 | return c.pr.Read(p) 47 | } 48 | func (c *conn) Write(p []byte) (n int, err error) { 49 | return c.remote.Write(p) 50 | } 51 | func (c *conn) Interrupt() error { 52 | return c.pw.Close() 53 | } 54 | func (c *conn) Close() error { 55 | c.close.Do(func() { 56 | c.pw.Close() 57 | c.remote.Close() 58 | }) 59 | return nil 60 | } 61 | func (c *conn) LocalAddr() net.Addr { return c.local } 62 | func (c *conn) RemoteAddr() net.Addr { return c.remote.config } 63 | func (c *conn) SetDeadline(t time.Time) error { return nil } 64 | func (c *conn) SetReadDeadline(t time.Time) error { return nil } 65 | func (c *conn) SetWriteDeadline(t time.Time) error { return nil } 66 | -------------------------------------------------------------------------------- /pkg/server/remote.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gowsp/wsp/pkg/logger" 7 | "github.com/gowsp/wsp/pkg/msg" 8 | ) 9 | 10 | func (c *conn) AddRemote(id string, conf *msg.WspConfig) error { 11 | channel := conf.Channel() 12 | if channel == "" { 13 | return fmt.Errorf("channel %s is illegal or empty", channel) 14 | } 15 | switch conf.Mode() { 16 | case "path": 17 | if conf.Value() == c.config().Path { 18 | return fmt.Errorf("setting the same path as wsps is not allowed") 19 | } 20 | case "domain": 21 | switch { 22 | case c.config().Host == "": 23 | return fmt.Errorf("wsps does not set host, domain method is not allowed") 24 | case c.config().Host == conf.Value(): 25 | return fmt.Errorf("setting the same domain as wsps is not allowed") 26 | } 27 | default: 28 | if conf.IsHTTP() { 29 | return fmt.Errorf("unsupported http mode %s", conf.Mode()) 30 | } 31 | } 32 | if hub.Exist(channel) { 33 | return fmt.Errorf("channel %s already registered", channel) 34 | } 35 | if err := c.wan.Reply(id, nil); err != nil { 36 | return err 37 | } 38 | logger.Info("register channel %s", channel) 39 | hub.Store(channel, c) 40 | c.listen.Store(channel, conf) 41 | return nil 42 | } 43 | 44 | func (c *conn) NewRemoteConn(req *msg.Data, config *msg.WspConfig) error { 45 | channel := config.Channel() 46 | val, ok := hub.Load(channel) 47 | if !ok { 48 | return fmt.Errorf("channel %s not registered", channel) 49 | } 50 | logger.Info("bridging request %s received", channel) 51 | remote := val.(*conn) 52 | if _, ok := remote.listen.Load(channel); !ok { 53 | hub.Remove(channel) 54 | return fmt.Errorf("channel %s offline", channel) 55 | } 56 | return c.wan.Bridge(req, config, remote.wan) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/client/http.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | "net" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/gowsp/wsp/pkg/logger" 12 | "github.com/gowsp/wsp/pkg/msg" 13 | "github.com/gowsp/wsp/pkg/stream" 14 | ) 15 | 16 | // HTTPProxy implement DynamicProxy 17 | type HTTPProxy struct { 18 | conf *msg.WspConfig 19 | wspc *Wspc 20 | } 21 | 22 | func (p *HTTPProxy) Listen() { 23 | address := p.conf.Address() 24 | logger.Info("listen http proxy %s", address) 25 | l, err := net.Listen(p.conf.Network(), address) 26 | if err != nil { 27 | logger.Error("listen http proxy error: %s", err) 28 | return 29 | } 30 | for { 31 | conn, err := l.Accept() 32 | if err != nil { 33 | logger.Error("http proxy accept %s", err) 34 | continue 35 | } 36 | go p.ServeConn(conn) 37 | } 38 | } 39 | 40 | func (p *HTTPProxy) ServeConn(local net.Conn) error { 41 | buffer := new(bytes.Buffer) 42 | reader := bufio.NewReader(io.TeeReader(local, buffer)) 43 | reqeust, err := http.ReadRequest(reader) 44 | if err != nil { 45 | return err 46 | } 47 | addr := reqeust.URL.Host 48 | ssl := reqeust.Method == http.MethodConnect 49 | if strings.LastIndexByte(reqeust.URL.Host, ':') == -1 { 50 | addr = addr + ":80" 51 | } 52 | logger.Info("open http proxy %s", addr) 53 | config := p.conf.DynamicAddr(addr) 54 | remote, err := p.wspc.wan.DialTCP(local.LocalAddr(), config) 55 | if err != nil { 56 | logger.Error("open http proxy %s error:", addr, err) 57 | local.Write([]byte("HTTP/1.1 500\r\n\r\n")) 58 | return err 59 | } 60 | defer remote.Close() 61 | if ssl { 62 | local.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")) 63 | } else { 64 | remote.Write(buffer.Bytes()) 65 | } 66 | buffer.Reset() 67 | stream.Copy(local, remote) 68 | logger.Info("close http proxy %s", addr) 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gobwas/ws" 11 | "github.com/gowsp/wsp/pkg/logger" 12 | "github.com/gowsp/wsp/pkg/msg" 13 | "github.com/gowsp/wsp/pkg/stream" 14 | ) 15 | 16 | type Wspc struct { 17 | dialer *ws.Dialer 18 | start sync.Once 19 | config WspcConfig 20 | listen sync.Map 21 | wan *stream.Wan 22 | handler *stream.Handler 23 | } 24 | 25 | func New(config WspcConfig) *Wspc { 26 | headers := make(http.Header) 27 | headers.Set("Auth", config.Auth) 28 | headers.Set("Proto", msg.PROTOCOL_VERSION.String()) 29 | w := &Wspc{config: config, 30 | dialer: &ws.Dialer{Header: ws.HandshakeHeaderHTTP(headers)}, 31 | } 32 | w.handler = stream.NewHandler(w) 33 | return w 34 | } 35 | func (c *Wspc) register() { 36 | for _, val := range c.config.Remote { 37 | config, err := msg.NewWspConfig(msg.WspType_REMOTE, val) 38 | if err != nil { 39 | logger.Error("remote config %s error: %s", val, err) 40 | continue 41 | } 42 | if _, err := c.wan.DialTCP(nil, config); err != nil { 43 | logger.Error("register %s error: %s", val, err) 44 | } 45 | c.listen.Store(config.Channel(), config) 46 | } 47 | } 48 | func (c *Wspc) ListenAndServe() { 49 | c.wan = c.connect() 50 | go c.register() 51 | c.start.Do(func() { 52 | c.LocalForward() 53 | c.DynamicForward() 54 | }) 55 | c.handler.Serve(c.wan) 56 | c.ListenAndServe() 57 | } 58 | func (c *Wspc) connect() *stream.Wan { 59 | server := c.config.Server 60 | conn, _, _, err := c.dialer.Dial(context.Background(), server) 61 | if err != nil { 62 | time.Sleep(3 * time.Second) 63 | logger.Info("connect %s error %s, reconnect ...", server, err.Error()) 64 | return c.connect() 65 | } 66 | logger.Info("successfully connected to %s", server) 67 | wan := stream.NewWan(conn, ws.StateClientSide) 68 | go wan.HeartBeat(time.Second * 30) 69 | return wan 70 | } 71 | 72 | func (c *Wspc) LoadConfig(channel string) (*msg.WspConfig, error) { 73 | if val, ok := c.listen.Load(channel); ok { 74 | return val.(*msg.WspConfig), nil 75 | } 76 | return nil, fmt.Errorf(channel + " not found") 77 | } 78 | -------------------------------------------------------------------------------- /pkg/server/http.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "net/http/httputil" 8 | "strings" 9 | 10 | "github.com/gobwas/ws" 11 | "github.com/gowsp/wsp/pkg/logger" 12 | "github.com/gowsp/wsp/pkg/msg" 13 | "github.com/gowsp/wsp/pkg/stream" 14 | ) 15 | 16 | func (r *conn) ServeHTTP(channel string, rw http.ResponseWriter, req *http.Request) { 17 | conf, _ := r.LoadConfig(channel) 18 | if conf.IsHTTP() { 19 | r.ServeHTTPProxy(conf, rw, req) 20 | } else { 21 | r.ServeNetProxy(conf, rw, req) 22 | } 23 | } 24 | func (r *conn) ServeNetProxy(conf *msg.WspConfig, w http.ResponseWriter, req *http.Request) { 25 | remote, err := r.wan.DialHTTP(conf) 26 | if err != nil { 27 | http.Error(w, err.Error(), http.StatusBadGateway) 28 | return 29 | } 30 | local, _, _, err := ws.UpgradeHTTP(req, w) 31 | if err != nil { 32 | logger.Error("websocket accept %v", err) 33 | return 34 | } 35 | rw := stream.NewReadWriter(local) 36 | logger.Info("start ws over tcp %s", conf.Channel()) 37 | stream.Copy(rw, remote) 38 | } 39 | 40 | func (r *conn) ServeHTTPProxy(conf *msg.WspConfig, w http.ResponseWriter, req *http.Request) { 41 | proxy := r.NewHTTPProxy(conf) 42 | if proxy == nil { 43 | return 44 | } 45 | proxy.ServeHTTP(w, req) 46 | } 47 | func (r *conn) NewHTTPProxy(conf *msg.WspConfig) *httputil.ReverseProxy { 48 | channel := conf.Channel() 49 | c, ok := r.http.Load(channel) 50 | if ok { 51 | return c.(*httputil.ReverseProxy) 52 | } 53 | logger.Info("start http proxy %s", channel) 54 | u := conf.ReverseURL() 55 | p := httputil.NewSingleHostReverseProxy(u) 56 | prefix := "http:path:" 57 | if strings.HasPrefix(channel, prefix) { 58 | prepare := p.Director 59 | path := strings.TrimPrefix(channel, prefix) 60 | p.Director = func(r *http.Request) { 61 | prefix := "/" + path 62 | r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix) 63 | prepare(r) 64 | } 65 | } 66 | transport := http.DefaultTransport.(*http.Transport).Clone() 67 | transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { 68 | return r.wan.DialHTTP(conf) 69 | } 70 | p.Transport = transport 71 | r.http.Store(channel, p) 72 | return p 73 | } 74 | -------------------------------------------------------------------------------- /pkg/server/server.go: -------------------------------------------------------------------------------- 1 | // Package server is core logic shared by wspc wsps 2 | package server 3 | 4 | import ( 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/gobwas/ws" 11 | "github.com/gowsp/wsp/pkg/logger" 12 | "github.com/gowsp/wsp/pkg/msg" 13 | ) 14 | 15 | type Wsps struct { 16 | config *Config 17 | handler http.Handler 18 | } 19 | 20 | func New(config *Config) http.Handler { 21 | return NewWithHandler(config, http.NotFoundHandler()) 22 | } 23 | func NewWithHandler(config *Config, handler http.Handler) http.Handler { 24 | config.clean() 25 | wsps := &Wsps{config: config, handler: handler} 26 | return wsps 27 | } 28 | 29 | func (s *Wsps) ServeHTTP(rw http.ResponseWriter, r *http.Request) { 30 | host, _, _ := net.SplitHostPort(r.Host) 31 | channel := "http:domain:" + host 32 | if val, ok := hub.Load(channel); ok { 33 | val.(*conn).ServeHTTP(channel, rw, r) 34 | return 35 | } 36 | path := strings.TrimPrefix(r.URL.Path, "/") 37 | if path == s.config.Path { 38 | s.ServeProxy(rw, r) 39 | return 40 | } 41 | paths := strings.Split(path, "/") 42 | if len(path) == 0 { 43 | s.handler.ServeHTTP(rw, r) 44 | return 45 | } 46 | channel = "http:path:" + paths[0] 47 | if val, ok := hub.Load(channel); ok { 48 | val.(*conn).ServeHTTP(channel, rw, r) 49 | return 50 | } 51 | s.handler.ServeHTTP(rw, r) 52 | } 53 | 54 | func (s *Wsps) ServeProxy(w http.ResponseWriter, r *http.Request) { 55 | if r.Header.Get("Auth") != s.config.Auth { 56 | w.WriteHeader(401) 57 | w.Write([]byte("token error, access denied!\n")) 58 | logger.Error("illegal request %s", getRemoteIP(r)) 59 | return 60 | } 61 | proto := r.Header.Get("Proto") 62 | logger.Info("accept %s, proto: %s", getRemoteIP(r), proto) 63 | if proto, err := msg.ParseVersion(proto); err != nil || proto.Major() != msg.PROTOCOL_VERSION.Major() { 64 | w.WriteHeader(400) 65 | fmt.Fprintf(w, "client proto version %s not support, server proto is %s\n", proto, msg.PROTOCOL_VERSION) 66 | return 67 | } 68 | ws, _, _, err := ws.UpgradeHTTP(r, w) 69 | if err != nil { 70 | logger.Error("websocket accept %v", err) 71 | return 72 | } 73 | 74 | router := &conn{wsps: s} 75 | router.ListenAndServe(ws) 76 | router.close() 77 | } 78 | 79 | func getRemoteIP(r *http.Request) string { 80 | IPAddress := r.Header.Get("X-Real-Ip") 81 | if IPAddress == "" { 82 | IPAddress = r.Header.Get("X-Forwarded-For") 83 | } 84 | if IPAddress == "" { 85 | IPAddress = r.RemoteAddr 86 | } 87 | return IPAddress 88 | } 89 | -------------------------------------------------------------------------------- /pkg/stream/conn.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/gowsp/wsp/pkg/logger" 8 | "github.com/gowsp/wsp/pkg/msg" 9 | "google.golang.org/protobuf/proto" 10 | ) 11 | 12 | type ready interface { 13 | ready(resp *msg.Data) error 14 | } 15 | 16 | func encode(id string, cmd msg.WspCmd, data []byte) ([]byte, error) { 17 | msg := &msg.WspMessage{Id: id, Cmd: cmd, Data: data} 18 | res, err := proto.Marshal(msg) 19 | if err != nil { 20 | logger.Error("encode msg %s error", cmd.String()) 21 | return nil, err 22 | } 23 | return res, nil 24 | } 25 | func parseResponse(data *msg.Data) error { 26 | var resp msg.WspResponse 27 | if err := proto.Unmarshal(data.Payload(), &resp); err != nil { 28 | return err 29 | } 30 | if resp.Code == msg.WspCode_FAILED { 31 | return errors.New(resp.Data) 32 | } 33 | return nil 34 | } 35 | 36 | type link struct { 37 | id string 38 | wan *Wan 39 | 40 | config *msg.WspConfig 41 | signal *Signal 42 | } 43 | 44 | func (w *link) open() error { 45 | addr, err := proto.Marshal(w.config.ToReqeust()) 46 | if err != nil { 47 | return err 48 | } 49 | data, err := encode(w.id, msg.WspCmd_CONNECT, addr) 50 | if err != nil { 51 | return err 52 | } 53 | w.wan.waiting.Store(w.id, w) 54 | defer w.wan.waiting.Delete(w.id) 55 | logger.Debug("send connect request %s, id %s", w.config, w.id) 56 | if err = w.wan.write(data, time.Second*5); err != nil { 57 | return err 58 | } 59 | return w.signal.Wait(time.Second * 5) 60 | } 61 | func (w *link) ready(resp *msg.Data) error { 62 | err := parseResponse(resp) 63 | w.signal.Notify(err) 64 | return nil 65 | } 66 | func (w *link) active() error { 67 | logger.Debug("send connect response %s", w.config) 68 | res := msg.WspResponse{Code: msg.WspCode_SUCCESS, Data: ""} 69 | response, _ := proto.Marshal(&res) 70 | data, err := encode(w.id, msg.WspCmd_RESPOND, response) 71 | if err != nil { 72 | return err 73 | } 74 | return w.wan.write(data, time.Second*5) 75 | } 76 | func (w *link) Write(p []byte) (n int, err error) { 77 | logger.Trace("send data %s", w.config) 78 | data, err := encode(w.id, msg.WspCmd_TRANSFER, p) 79 | if err != nil { 80 | return 0, err 81 | } 82 | n = len(p) 83 | err = w.wan.write(data, time.Second*5) 84 | return 85 | } 86 | func (w *link) Close() error { 87 | logger.Debug("send interrupt request %s", w.config) 88 | w.wan.connect.Delete(w.id) 89 | data, err := encode(w.id, msg.WspCmd_INTERRUPT, []byte{}) 90 | if err != nil { 91 | return err 92 | } 93 | return w.wan.write(data, time.Second*5) 94 | } 95 | -------------------------------------------------------------------------------- /pkg/stream/utils.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sync" 7 | "sync/atomic" 8 | "time" 9 | ) 10 | 11 | var globalTaskID uint64 12 | 13 | func nextTaskID() uint64 { 14 | return atomic.AddUint64(&globalTaskID, 1) 15 | } 16 | 17 | func Copy(local, remote io.ReadWriteCloser) { 18 | var wg sync.WaitGroup 19 | wg.Add(2) 20 | go func() { 21 | defer wg.Done() 22 | io.Copy(local, remote) 23 | local.Close() 24 | }() 25 | go func() { 26 | defer wg.Done() 27 | io.Copy(remote, local) 28 | remote.Close() 29 | }() 30 | wg.Wait() 31 | } 32 | 33 | func NewTaskPool(parallel, capacity int) *TaskPool { 34 | tasks := make(chan func(), capacity) 35 | for i := 0; i < parallel; i++ { 36 | go func() { 37 | for task := range tasks { 38 | task() 39 | } 40 | }() 41 | } 42 | return &TaskPool{tasks: tasks} 43 | } 44 | 45 | type TaskPool struct { 46 | wg sync.WaitGroup 47 | tasks chan func() 48 | } 49 | 50 | func (p *TaskPool) Add(task func()) { 51 | p.wg.Add(1) 52 | p.tasks <- func() { 53 | defer p.wg.Done() 54 | task() 55 | } 56 | } 57 | func (p *TaskPool) Wait() { 58 | p.wg.Wait() 59 | } 60 | func (p *TaskPool) Close() { 61 | p.Wait() 62 | close(p.tasks) 63 | } 64 | 65 | func NewPollTaskPool(parallel, capacity int) *PollTaskPool { 66 | tasks := make([]chan func(), parallel) 67 | for i := 0; i < parallel; i++ { 68 | item := make(chan func(), capacity) 69 | go func() { 70 | for task := range item { 71 | task() 72 | } 73 | }() 74 | tasks[i] = item 75 | } 76 | return &PollTaskPool{parallel: uint64(parallel), tasks: tasks} 77 | } 78 | 79 | type PollTaskPool struct { 80 | parallel uint64 81 | 82 | wg sync.WaitGroup 83 | tasks []chan func() 84 | } 85 | 86 | func (p *PollTaskPool) Add(id uint64, task func()) { 87 | p.wg.Add(1) 88 | p.tasks[id%p.parallel] <- func() { 89 | defer p.wg.Done() 90 | task() 91 | } 92 | } 93 | 94 | func (p *PollTaskPool) Wait() { 95 | p.wg.Wait() 96 | } 97 | func (p *PollTaskPool) Close() { 98 | p.Wait() 99 | for _, v := range p.tasks { 100 | close(v) 101 | } 102 | } 103 | func NewSignal() *Signal { 104 | return &Signal{done: make(chan error, 1)} 105 | } 106 | 107 | type Signal struct { 108 | done chan error 109 | } 110 | 111 | func (s *Signal) Notify(err error) { 112 | s.done <- err 113 | } 114 | func (s *Signal) Wait(timeout time.Duration) error { 115 | select { 116 | case res := <-s.done: 117 | return res 118 | case <-time.After(timeout): 119 | return errors.New("timeout") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /pkg/msg/config.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "net/url" 5 | ) 6 | 7 | func (r *WspRequest) ToConfig() (*WspConfig, error) { 8 | u, err := url.Parse(r.Data) 9 | if err != nil { 10 | return nil, err 11 | } 12 | return &WspConfig{wspType: r.Type, url: u}, nil 13 | } 14 | 15 | func NewWspConfig(wspType WspType, addr string) (*WspConfig, error) { 16 | u, err := url.Parse(addr) 17 | if err != nil { 18 | return nil, err 19 | } 20 | return &WspConfig{wspType: wspType, url: u}, nil 21 | } 22 | 23 | type WspConfig struct { 24 | wspType WspType 25 | url *url.URL 26 | } 27 | 28 | func (c *WspConfig) ToReqeust() *WspRequest { 29 | return &WspRequest{Type: c.wspType, Data: c.url.String()} 30 | } 31 | func (c *WspConfig) IsRemoteType() bool { 32 | return c.wspType == WspType_REMOTE 33 | } 34 | func (c *WspConfig) IsHTTP() bool { 35 | switch c.url.Scheme { 36 | case "http", "https": 37 | return true 38 | } 39 | return false 40 | } 41 | func (c *WspConfig) IsTunnel() bool { 42 | return c.url.Scheme == "tunnel" 43 | } 44 | func (c *WspConfig) Channel() string { 45 | if c.IsHTTP() { 46 | return "http:" + c.Mode() + ":" + c.Value() 47 | } 48 | mode := c.Mode() 49 | if mode == "" { 50 | return c.url.User.Username() 51 | } 52 | return "http:" + c.Mode() + ":" + c.Value() 53 | } 54 | func (c *WspConfig) Scheme() string { 55 | return c.url.Scheme 56 | } 57 | func (c *WspConfig) Network() string { 58 | network := c.url.Scheme 59 | switch network { 60 | case "http", "https", "socks5": 61 | return "tcp" 62 | default: 63 | return network 64 | } 65 | } 66 | func (c *WspConfig) String() string { 67 | return c.url.String() 68 | } 69 | func (c *WspConfig) Address() string { 70 | return c.url.Host 71 | } 72 | func (c *WspConfig) Paasowrd() string { 73 | pwd, _ := c.url.User.Password() 74 | return pwd 75 | } 76 | func (c *WspConfig) DynamicAddr(addr string) *WspConfig { 77 | wspType := c.wspType 78 | if c.url.User != nil { 79 | wspType = WspType_LOCAL 80 | } 81 | return &WspConfig{ 82 | wspType: wspType, 83 | url: &url.URL{ 84 | Scheme: c.url.Scheme, 85 | Host: addr, 86 | User: c.url.User, 87 | RawQuery: c.url.RawQuery, 88 | }, 89 | } 90 | } 91 | 92 | func (c *WspConfig) ReverseURL() *url.URL { 93 | return &url.URL{ 94 | Scheme: c.url.Scheme, 95 | Host: c.url.Host, 96 | Path: c.url.Path, 97 | } 98 | } 99 | func (c *WspConfig) Mode() string { 100 | return c.url.Query().Get("mode") 101 | } 102 | func (c *WspConfig) Value() string { 103 | return c.url.Query().Get("value") 104 | } 105 | -------------------------------------------------------------------------------- /pkg/stream/bridge.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/gowsp/wsp/pkg/logger" 10 | "github.com/gowsp/wsp/pkg/msg" 11 | ) 12 | 13 | type bridge struct { 14 | id string 15 | taskID uint64 16 | input *Wan 17 | output *Wan 18 | config *msg.WspConfig 19 | 20 | signal *Signal 21 | close sync.Once 22 | } 23 | 24 | func (c *bridge) connect(data *msg.Data) error { 25 | c.id = data.ID() 26 | c.input.waiting.Store(c.id, c) 27 | defer c.input.waiting.Delete(c.id) 28 | if err := c.input.write(*data.Raw, time.Second*5); err != nil { 29 | return err 30 | } 31 | return c.signal.Wait(time.Second * 5) 32 | } 33 | 34 | func (c *bridge) ready(resp *msg.Data) error { 35 | err := parseResponse(resp) 36 | c.signal.Notify(err) 37 | if err != nil { 38 | return err 39 | } 40 | c.input.connect.Store(c.id, c) 41 | c.output.connect.Store(c.id, &bridge{ 42 | taskID: nextTaskID(), 43 | id: c.id, 44 | input: c.output, 45 | config: c.config, 46 | output: c.input, 47 | }) 48 | c.Rewrite(resp) 49 | return nil 50 | } 51 | func (c *bridge) TaskID() uint64 { 52 | return c.taskID 53 | } 54 | func (c *bridge) Rewrite(data *msg.Data) (n int, err error) { 55 | _, err = c.Write(*data.Raw) 56 | return 57 | } 58 | func (c *bridge) Read(b []byte) (n int, err error) { 59 | return 0, io.EOF 60 | } 61 | func (c *bridge) Write(b []byte) (n int, err error) { 62 | n = len(b) 63 | err = c.output.write(b, time.Second*5) 64 | return 65 | } 66 | func (c *bridge) Interrupt() error { 67 | c.close.Do(func() { 68 | logger.Info("close bridge %s", c.config.Channel()) 69 | c.output.connect.Delete(c.id) 70 | data, _ := encode(c.id, msg.WspCmd_INTERRUPT, []byte{}) 71 | c.Write(data) 72 | }) 73 | return nil 74 | } 75 | func (c *bridge) Close() error { 76 | c.close.Do(func() { 77 | logger.Info("close bridge %s", c.config.Channel()) 78 | data, _ := encode(c.id, msg.WspCmd_INTERRUPT, []byte{}) 79 | if val, ok := c.input.connect.LoadAndDelete(c.id); ok { 80 | val.(*bridge).Write(data) 81 | } 82 | if val, ok := c.output.connect.LoadAndDelete(c.id); ok { 83 | val.(*bridge).Write(data) 84 | } 85 | }) 86 | return nil 87 | } 88 | func (c *bridge) LocalAddr() net.Addr { 89 | return c.config 90 | } 91 | func (c *bridge) RemoteAddr() net.Addr { 92 | return c.config 93 | } 94 | func (c *bridge) SetDeadline(t time.Time) error { 95 | return nil 96 | } 97 | func (c *bridge) SetReadDeadline(t time.Time) error { 98 | return nil 99 | } 100 | func (c *bridge) SetWriteDeadline(t time.Time) error { 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /wsp_test.go: -------------------------------------------------------------------------------- 1 | package wsp 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/gowsp/wsp/pkg/client" 8 | "github.com/gowsp/wsp/pkg/logger" 9 | "github.com/gowsp/wsp/pkg/server" 10 | ) 11 | 12 | func init() { 13 | // c := logger.Config{Level: "debug", Output: "d:/tmp/out.txt"} 14 | c := logger.Config{Level: "debug",} 15 | c.Init() 16 | } 17 | func TestServer(t *testing.T) { 18 | wsps := server.New(&server.Config{Auth: "auth", Path: "/proxy"}) 19 | http.ListenAndServe(":8080", wsps) 20 | } 21 | func TestProxyClient(t *testing.T) { 22 | client := client.New(client.WspcConfig{ 23 | Auth: "auth", 24 | Server: "ws://127.0.0.1:8080/proxy", 25 | Dynamic: []string{ 26 | "socks5://:1080", 27 | "http://:8088", 28 | }, 29 | }) 30 | client.ListenAndServe() 31 | } 32 | func TestTunnel(t *testing.T) { 33 | go client.New(client.WspcConfig{ 34 | Auth: "auth", 35 | Server: "ws://127.0.0.1:8080/proxy", 36 | Remote: []string{ 37 | "tunnel://dynamic:vpn@", 38 | }, 39 | }).ListenAndServe() 40 | client.New(client.WspcConfig{ 41 | Auth: "auth", 42 | Server: "ws://127.0.0.1:8080/proxy", 43 | Dynamic: []string{ 44 | "socks5://home:vpn@:8020", 45 | }, 46 | }).ListenAndServe() 47 | } 48 | func TestReverseProxy(t *testing.T) { 49 | server := http.NewServeMux() 50 | server.HandleFunc("/api/users", func(rw http.ResponseWriter, r *http.Request) { 51 | rw.Write([]byte(r.RequestURI)) 52 | }) 53 | server.HandleFunc("/api/groups", func(rw http.ResponseWriter, r *http.Request) { 54 | rw.Write([]byte(r.RequestURI)) 55 | }) 56 | web := http.Server{Handler: server, Addr: ":8010"} 57 | go web.ListenAndServe() 58 | // http://127.0.0.1:8010/api/users 59 | // http://127.0.0.1:8010/api/groups 60 | client := client.New(client.WspcConfig{ 61 | Auth: "auth", 62 | Server: "ws://127.0.0.1:8080/proxy", 63 | Remote: []string{ 64 | "http://127.0.0.1:8010?mode=path&value=local", 65 | "http://127.0.0.1:8010/api/?mode=path&value=wuzk", 66 | }, 67 | }) 68 | // http://127.0.0.1:8080/local/api/users 69 | // http://127.0.0.1:8080/local/api/groups 70 | // http://127.0.0.1:8080/wuzk/users 71 | // http://127.0.0.1:8080/wuzk/groups 72 | client.ListenAndServe() 73 | } 74 | func TestTCPOverWs(t *testing.T) { 75 | client.New(client.WspcConfig{ 76 | Auth: "auth", 77 | Server: "ws://127.0.0.1:8080/proxy", 78 | Remote: []string{ 79 | "tcp://127.0.0.1:5900?mode=path&value=test", 80 | }, 81 | }).ListenAndServe() 82 | } 83 | 84 | func TestTunnel1(t *testing.T) { 85 | client.New(client.WspcConfig{ 86 | Auth: "auth", 87 | Server: "ws://127.0.0.1:8080/proxy", 88 | Remote: []string{ 89 | "tcp://ssh:pwd@192.168.7.171:22", 90 | }, 91 | }).ListenAndServe() 92 | } 93 | func TestTunnel2(t *testing.T) { 94 | client.New(client.WspcConfig{ 95 | Auth: "auth", 96 | Server: "ws://127.0.0.1:8080/proxy", 97 | Local: []string{ 98 | "tcp://ssh:pwd@127.0.0.1:2202", 99 | }, 100 | }).ListenAndServe() 101 | } 102 | -------------------------------------------------------------------------------- /pkg/stream/wan.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "hash/fnv" 5 | "io" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "github.com/gobwas/ws" 11 | "github.com/gobwas/ws/wsutil" 12 | "github.com/gowsp/wsp/pkg/logger" 13 | "github.com/gowsp/wsp/pkg/msg" 14 | "github.com/segmentio/ksuid" 15 | "google.golang.org/protobuf/proto" 16 | ) 17 | 18 | func NewWan(conn net.Conn, state ws.State) *Wan { 19 | return &Wan{ 20 | conn: conn, 21 | state: state, 22 | } 23 | } 24 | 25 | type Wan struct { 26 | conn net.Conn 27 | state ws.State 28 | lock sync.Mutex 29 | 30 | waiting sync.Map 31 | connect sync.Map 32 | } 33 | 34 | func (w *Wan) getTaskID(id string) uint64 { 35 | if val, ok := w.connect.Load(id); ok { 36 | return val.(Conn).TaskID() 37 | } 38 | h := fnv.New64a() 39 | h.Write([]byte(id)) 40 | return h.Sum64() 41 | } 42 | func (w *Wan) read() ([]byte, ws.OpCode, error) { 43 | return wsutil.ReadData(w.conn, w.state) 44 | } 45 | func (w *Wan) write(data []byte, timeout time.Duration) error { 46 | w.lock.Lock() 47 | defer w.lock.Unlock() 48 | return wsutil.WriteMessage(w.conn, w.state, ws.OpBinary, data) 49 | } 50 | func (w *Wan) newLink(id string, config *msg.WspConfig) *link { 51 | return &link{ 52 | id: id, 53 | wan: w, 54 | config: config, 55 | signal: NewSignal(), 56 | } 57 | } 58 | 59 | func (w *Wan) HeartBeat(d time.Duration) { 60 | t := time.NewTicker(d) 61 | for { 62 | <-t.C 63 | w.lock.Lock() 64 | if err := wsutil.WriteMessage(w.conn, w.state, ws.OpPing, []byte{}); err != nil { 65 | w.lock.Unlock() 66 | w.conn.Close() 67 | break 68 | } 69 | w.lock.Unlock() 70 | } 71 | } 72 | func (w *Wan) DialTCP(local net.Addr, remote *msg.WspConfig) (io.ReadWriteCloser, error) { 73 | id := ksuid.New().String() 74 | writer := w.newLink(id, remote) 75 | if err := writer.open(); err != nil || remote.IsRemoteType() { 76 | return nil, err 77 | } 78 | conn := newConn(local, writer) 79 | w.connect.Store(id, conn) 80 | return conn, nil 81 | } 82 | func (w *Wan) DialHTTP(remote *msg.WspConfig) (net.Conn, error) { 83 | id := ksuid.New().String() 84 | link := w.newLink(id, remote) 85 | if err := link.open(); err != nil { 86 | return nil, err 87 | } 88 | conn := newConn(&net.TCPAddr{}, link) 89 | w.connect.Store(id, conn) 90 | return conn, nil 91 | } 92 | func (w *Wan) Accept(id string, local net.Addr, config *msg.WspConfig) (io.ReadWriteCloser, error) { 93 | link := w.newLink(id, config) 94 | conn := newConn(local, link) 95 | w.connect.Store(id, conn) 96 | if err := link.active(); err != nil { 97 | conn.Close() 98 | return nil, err 99 | } 100 | return conn, nil 101 | } 102 | func (w *Wan) Reply(id string, message error) (err error) { 103 | var res msg.WspResponse 104 | if message == nil { 105 | res = msg.WspResponse{Code: msg.WspCode_SUCCESS} 106 | } else { 107 | res = msg.WspResponse{Code: msg.WspCode_FAILED, Data: message.Error()} 108 | } 109 | logger.Debug("send connect response %s, error: %s", id, err) 110 | response, _ := proto.Marshal(&res) 111 | data, _ := encode(id, msg.WspCmd_RESPOND, response) 112 | return w.write(data, time.Minute) 113 | } 114 | func (w *Wan) Bridge(req *msg.Data, config *msg.WspConfig, rwan *Wan) error { 115 | p := &bridge{ 116 | taskID: nextTaskID(), 117 | input: rwan, 118 | output: w, 119 | config: config, 120 | signal: NewSignal(), 121 | } 122 | return p.connect(req) 123 | } 124 | -------------------------------------------------------------------------------- /pkg/stream/handler.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "runtime" 8 | "time" 9 | 10 | "github.com/gobwas/ws" 11 | "github.com/gowsp/wsp/pkg/logger" 12 | "github.com/gowsp/wsp/pkg/msg" 13 | "google.golang.org/protobuf/proto" 14 | ) 15 | 16 | var ErrConnNotExist = errors.New("connection does not exist") 17 | 18 | type Dialer interface { 19 | NewConn(data *msg.Data, req *msg.WspRequest) error 20 | } 21 | 22 | func NewHandler(dialer Dialer) *Handler { 23 | h := &Handler{ 24 | dialer: dialer, 25 | cmds: make(map[msg.WspCmd]handlerFunc), 26 | loop: NewTaskPool(1, 256), 27 | event: NewPollTaskPool(len(msg.WspCmd_value), 512), 28 | worker: NewPollTaskPool(runtime.NumCPU()*4, 1024), 29 | } 30 | h.init() 31 | return h 32 | } 33 | 34 | type Handler struct { 35 | dialer Dialer 36 | cmds map[msg.WspCmd]handlerFunc 37 | loop *TaskPool 38 | event *PollTaskPool 39 | worker *PollTaskPool 40 | } 41 | 42 | func (w *Handler) Serve(wan *Wan) { 43 | for { 44 | data, mt, err := wan.read() 45 | if mt == ws.OpClose { 46 | break 47 | } 48 | if err == nil { 49 | w.process(wan, mt, data) 50 | continue 51 | } 52 | if err != io.EOF { 53 | logger.Error("error reading webSocket message: %s", err) 54 | } 55 | break 56 | } 57 | if wan.state.ClientSide() { 58 | w.loop.Wait() 59 | w.event.Wait() 60 | w.worker.Wait() 61 | } else { 62 | w.loop.Close() 63 | w.event.Close() 64 | w.worker.Close() 65 | } 66 | wan.connect.Range(func(key, value any) bool { 67 | value.(Conn).Interrupt() 68 | return true 69 | }) 70 | } 71 | 72 | func (w *Handler) process(wan *Wan, mt ws.OpCode, data []byte) { 73 | w.loop.Add(func() { 74 | switch mt { 75 | case ws.OpBinary: 76 | m := new(msg.WspMessage) 77 | if err := proto.Unmarshal(data, m); err != nil { 78 | logger.Error("error unmarshal message: %s", err) 79 | return 80 | } 81 | w.event.Add(uint64(m.Cmd), func() { 82 | taskID := wan.getTaskID(m.Id) 83 | w.worker.Add(taskID, func() { 84 | err := w.serve(&msg.Data{Msg: m, Raw: &data}, wan) 85 | if errors.Is(err, ErrConnNotExist) { 86 | logger.Debug("connect %s not exists", m.Id) 87 | data, _ := encode(m.Id, msg.WspCmd_INTERRUPT, []byte(err.Error())) 88 | wan.write(data, time.Minute) 89 | } 90 | }) 91 | }) 92 | default: 93 | logger.Error("unsupported message type %v", mt) 94 | } 95 | }) 96 | } 97 | 98 | type handlerFunc func(data *msg.Data, wan *Wan) error 99 | 100 | func (w *Handler) init() { 101 | w.cmds[msg.WspCmd_CONNECT] = func(data *msg.Data, wan *Wan) error { 102 | id := data.ID() 103 | req := new(msg.WspRequest) 104 | if err := proto.Unmarshal(data.Payload(), req); err != nil { 105 | logger.Error("invalid request data %s", err) 106 | wan.Reply(id, err) 107 | return err 108 | } 109 | go func() { 110 | logger.Debug("receive %s connect reqeust: %s", id, req) 111 | if err := w.dialer.NewConn(data, req); err != nil { 112 | logger.Error("error to open connect %s", req) 113 | wan.Reply(id, err) 114 | } 115 | }() 116 | return nil 117 | } 118 | w.cmds[msg.WspCmd_RESPOND] = func(data *msg.Data, wan *Wan) error { 119 | id := data.ID() 120 | logger.Debug("receive %s connect response", id) 121 | if val, ok := wan.waiting.LoadAndDelete(id); ok { 122 | return val.(ready).ready(data) 123 | } 124 | return ErrConnNotExist 125 | } 126 | w.cmds[msg.WspCmd_TRANSFER] = func(data *msg.Data, wan *Wan) error { 127 | id := data.ID() 128 | logger.Trace("receive %s transfer data", id) 129 | if val, ok := wan.connect.Load(id); ok { 130 | _, err := val.(Conn).Rewrite(data) 131 | if err != nil { 132 | val.(Conn).Close() 133 | } 134 | return nil 135 | } 136 | return ErrConnNotExist 137 | } 138 | w.cmds[msg.WspCmd_INTERRUPT] = func(data *msg.Data, wan *Wan) error { 139 | id := data.ID() 140 | logger.Debug("receive %s disconnect request", id) 141 | if val, ok := wan.connect.LoadAndDelete(id); ok { 142 | return val.(Conn).Interrupt() 143 | } 144 | return nil 145 | } 146 | } 147 | func (w *Handler) serve(data *msg.Data, wan *Wan) error { 148 | cmd, ok := w.cmds[data.Cmd()] 149 | if !ok { 150 | return fmt.Errorf("unknown command %s", data.Msg.Cmd) 151 | } 152 | return cmd(data, wan) 153 | } 154 | -------------------------------------------------------------------------------- /pkg/client/socks5.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "net" 8 | 9 | "github.com/gowsp/wsp/pkg/logger" 10 | "github.com/gowsp/wsp/pkg/msg" 11 | "github.com/gowsp/wsp/pkg/stream" 12 | ) 13 | 14 | var errVersion = fmt.Errorf("unsupported socks version") 15 | 16 | // Socks5Proxy implement DynamicProxy 17 | type Socks5Proxy struct { 18 | conf *msg.WspConfig 19 | wspc *Wspc 20 | } 21 | 22 | func (p *Socks5Proxy) Listen() { 23 | address := p.conf.Address() 24 | logger.Info("listen socks5 on %s", address) 25 | l, err := net.Listen(p.conf.Network(), address) 26 | if err != nil { 27 | logger.Error("listen socks5 error %s", err) 28 | return 29 | } 30 | for { 31 | conn, err := l.Accept() 32 | if err != nil { 33 | logger.Error("accept socks5 error %s", err) 34 | continue 35 | } 36 | go func() { 37 | err := p.ServeConn(conn) 38 | if err == nil || err == io.EOF { 39 | return 40 | } 41 | if err != errVersion { 42 | logger.Error("serve socks5 error", err) 43 | } 44 | }() 45 | } 46 | } 47 | 48 | func (p *Socks5Proxy) ServeConn(conn net.Conn) error { 49 | if err := p.auth(conn); err != nil { 50 | conn.Close() 51 | return err 52 | } 53 | addr, err := p.readRequest(conn) 54 | if err != nil { 55 | conn.Close() 56 | return err 57 | } 58 | p.replies(addr, conn) 59 | return nil 60 | } 61 | 62 | func (p *Socks5Proxy) auth(conn net.Conn) error { 63 | // +----+----------+----------+ 64 | // |VER | NMETHODS | METHODS | 65 | // +----+----------+----------+ 66 | // | 1 | 1 | 1 to 255 | 67 | // +----+----------+----------+ 68 | info := make([]byte, 2) 69 | if _, err := io.ReadFull(conn, info); err != nil { 70 | return err 71 | } 72 | if info[0] != 0x05 { 73 | conn.Write([]byte{0x05, 0xFF}) 74 | return errVersion 75 | } 76 | methods := make([]byte, info[1]) 77 | if _, err := io.ReadFull(conn, methods); err != nil { 78 | return err 79 | } 80 | // +----+--------+ 81 | // |VER | METHOD | 82 | // +----+--------+ 83 | // | 1 | 1 | 84 | // +----+--------+ 85 | _, err := conn.Write([]byte{0x05, 0x00}) 86 | return err 87 | } 88 | 89 | func (p *Socks5Proxy) readRequest(conn net.Conn) (addr string, err error) { 90 | // +----+-----+-------+------+----------+----------+ 91 | // |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT | 92 | // +----+-----+-------+------+----------+----------+ 93 | // | 1 | 1 | X'00' | 1 | Variable | 2 | 94 | // +----+-----+-------+------+----------+----------+ 95 | info := make([]byte, 4) 96 | if _, err := io.ReadFull(conn, info); err != nil { 97 | return "", err 98 | } 99 | if info[0] != 0x05 { 100 | return "", errVersion 101 | } 102 | var host string 103 | switch info[3] { 104 | case 1: 105 | host, err = p.readIP(conn, net.IPv4len) 106 | if err != nil { 107 | return "", err 108 | } 109 | case 4: 110 | host, err = p.readIP(conn, net.IPv6len) 111 | if err != nil { 112 | return "", err 113 | } 114 | case 3: 115 | if _, err := io.ReadFull(conn, info[3:]); err != nil { 116 | return "", err 117 | } 118 | hostName := make([]byte, info[3]) 119 | if _, err := io.ReadFull(conn, hostName); err != nil { 120 | return "", err 121 | } 122 | host = string(hostName) 123 | default: 124 | return "", fmt.Errorf("unrecognized address type") 125 | } 126 | if _, err := io.ReadFull(conn, info[2:]); err != nil { 127 | return "", err 128 | } 129 | port := binary.BigEndian.Uint16(info[2:]) 130 | return net.JoinHostPort(host, fmt.Sprintf("%d", port)), nil 131 | } 132 | func (p *Socks5Proxy) readIP(conn net.Conn, len byte) (string, error) { 133 | addr := make([]byte, len) 134 | if _, err := io.ReadFull(conn, addr); err != nil { 135 | return "", err 136 | } 137 | return net.IP(addr).String(), nil 138 | } 139 | func (p *Socks5Proxy) replies(addr string, local net.Conn) { 140 | config := p.conf.DynamicAddr(addr) 141 | remote, err := p.wspc.wan.DialTCP(local.LocalAddr(), config) 142 | if err != nil { 143 | local.Write([]byte{0x05, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) 144 | logger.Error("close socks5 %s %s", addr, err.Error()) 145 | return 146 | } 147 | local.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) 148 | stream.Copy(local, remote) 149 | logger.Info("close socks5 %s, addr %s", addr, local.RemoteAddr()) 150 | } 151 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wsp 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/gowsp/wsp/release) 4 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/gowsp/wsp) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/gowsp/wsp)](https://goreportcard.com/report/github.com/gowsp/wsp) 6 | 7 | wsp 全称**W**eb**S**ocket **P**roxy 是一种基于 WebSocket 的全方位代理, 仅需要web端口即可提供以下功能: 8 | 9 | - 正向代理:支持 socks5 http代理,实现突破防火墙的访问 10 | - 反向代理:支持将NAT或防火墙后面的本地服务暴露在Internet 11 | - 网络穿透:将处在NAT设备之后的机器建立连接通讯隧道,进行数据的相互访问 12 | 13 | wsp为C/S架构,其中 wsps 位于公网提供 WebSocket 服务,wspc 连接 wsps 进行数据转发,以下为简单的结构示意图 14 | 15 | ```mermaid 16 | flowchart TD 17 | wspc1 --> wsps 18 | wspc2 --> wsps 19 | wspc3 --> wsps 20 | wsps --> wspc1 21 | wsps --> wspc2 22 | wsps --> wspc3 23 | ``` 24 | 25 | ## 配置说明 26 | 27 | ## Wsps 28 | 29 | 服务端安装,根据操作系统从[Release](https://github.com/gowsp/wsp/releases/latest)下载相应的程序包,解压后将wsps放置在公网机器上,配置用于提供服务的web端口,最小化 30 | 31 | 最小化配置如下: 32 | 33 | ```json 34 | { 35 | "port": 8010 36 | } 37 | ``` 38 | 39 | 启动服务端, `./wsps -c wsps.json`,其他配置可参考 `configs/wsps_template.json` 40 | 41 | 完整配置示例: 42 | 43 | ```json 44 | { 45 | "log": { 46 | "level": "info", 47 | "output": "/var/log/wsps.log" 48 | }, 49 | "ssl": { 50 | "enable": false, 51 | "key": "/var/cert/key.file", 52 | "cert": "/var/cert/cert.file" 53 | }, 54 | "host": "mywsps.com", 55 | "auth": "auth", 56 | "path": "/proxy", 57 | "port": 8010 58 | } 59 | ``` 60 | 61 | 完整配置项说明: 62 | 63 | | 属性 | 类型 | 必填 | 描述 | 64 | |------|------|------|------| 65 | | port | 整数 | 是 | 服务监听端口 | 66 | | auth | 字符串 | 否 | 认证 Token,客户端需要提供相同的 Token 才能连接 | 67 | | path | 字符串 | 否 | WebSocket 服务路径,默认为 "/" | 68 | | host | 字符串 | 否 | 主机名,用于域名模式的反向代理 | 69 | | log | 对象 | 否 | 日志配置 | 70 | | ssl | 对象 | 否 | SSL 配置 | 71 | 72 | #### 日志配置 (log) 73 | 74 | | 属性 | 类型 | 必填 | 描述 | 75 | |------|------|------|------| 76 | | level | 字符串 | 否 | 日志级别,支持 error、info、debug、trace | 77 | | output | 字符串 | 否 | 日志输出文件路径,默认输出到标准输出 | 78 | 79 | #### SSL 配置 (ssl) 80 | 81 | | 属性 | 类型 | 必填 | 描述 | 82 | |------|------|------|------| 83 | | enable | 布尔值 | 否 | 是否启用 SSL,默认为 false | 84 | | key | 字符串 | 否 | SSL 私钥文件路径 | 85 | | cert | 字符串 | 否 | SSL 证书文件路径 | 86 | 87 | ## Wspc 88 | 89 | 客户端wspc功能设计参考了ssh,配置项存在三种转发模式: 90 | 91 | - DynamicForward,动态转发,提供正向代理如:socks5,http代理 92 | - RemoteForward,远程转发,将本地端口服务暴露在wsps上,支持 TCP HTTP HTTPS 协议 93 | - LocalForward,本地转发,用于本地访问已注册的`远程转发`服务 94 | 95 | 支持连接多个 wsps 服务端,配置格式类似如下: 96 | 97 | ```json 98 | { 99 | "client": [ 100 | { 101 | "server": "ws://wsp1.com:8010", 102 | "local": [] 103 | }, 104 | { 105 | "server": "ws://wsp1.com:8010", 106 | "remote": [] 107 | } 108 | ] 109 | } 110 | ``` 111 | 112 | `wspc`的其他配置可参考 `configs/wspc_template.json` 113 | 114 | 完整配置示例: 115 | 116 | ```json 117 | { 118 | "log": { 119 | "level": "info", 120 | "output": "/var/log/wspc.log" 121 | }, 122 | "client": [ 123 | { 124 | "auth": "auth", 125 | "server": "ws://mywsps.com:8010/proxy", 126 | "dynamic": [ 127 | "socks5://:1080" 128 | ], 129 | "remote": [ 130 | "tcp://ssh:passwod@192.168.1.100:22", 131 | "http://127.0.0.1:8080?mode=path&value=api", 132 | "http://127.0.0.1:8080?mode=domain&value=custom.com" 133 | ], 134 | "local": [ 135 | "tcp://ssh:passwod@127.0.0.1:2200" 136 | ] 137 | } 138 | ] 139 | } 140 | ``` 141 | 142 | 完整配置项说明: 143 | 144 | | 属性 | 类型 | 必填 | 描述 | 145 | |------|------|------|------| 146 | | auth | 字符串 | 否 | 认证 Token,需要与服务端配置一致 | 147 | | server | 字符串 | 是 | 服务端地址 | 148 | | dynamic | 数组 | 否 | 动态转发配置(正向代理) | 149 | | remote | 数组 | 否 | 远程转发配置(反向代理/网络穿透服务端) | 150 | | local | 数组 | 否 | 本地转发配置(网络穿透客户端) | 151 | | log | 对象 | 否 | 日志配置 | 152 | 153 | ## 正向代理 154 | 155 | ### client模式 156 | 157 | 此模式下需要在本机中安装wspc配合wsps使用正向代理,wspc动态转发代理请求,连接的打开和流量传输通过wsps,示意图如下 158 | 159 | ```mermaid 160 | sequenceDiagram 161 | actor client 162 | participant wspc 163 | participant wsps 164 | actor server 165 | par connect 166 | wspc --> wsps : keep websocket connect 167 | and proxy 168 | client ->> wspc: socks5 or http proxy reqeust 169 | wspc ->> wsps: request open connect 170 | wsps ->> server: open connect 171 | server ->> wsps: connect response 172 | wsps ->> wspc: connect response 173 | wspc ->> client: proxy response 174 | and tranfer data 175 | client -->> server: tranfer data, via wspc 176 | server -->> client: tranfer data, via wsps 177 | end 178 | ``` 179 | 180 | 配置格式:`protocols://[bind_address]:port` 181 | 182 | - `protocols`支持 socks5 代理协议,HTTP 代理 183 | - `bind_address`可选,空地址表示监听所有网卡IP 184 | - `port`本地监听端口 185 | 186 | 示例如下: 187 | 188 | ```json 189 | { 190 | "client": [ 191 | { 192 | "server": "ws://mywsps.com:8010", 193 | "dynamic": [ 194 | "http://:80", 195 | "socks5://:1080" 196 | ] 197 | } 198 | ] 199 | } 200 | ``` 201 | 202 | 启动wspc, `./wsps -c wsps.json`, 此时本地`1080`提供socks5代理,`80`提供http代理, 流量则通过`wsps`进行访问 203 | 204 | ## 反向代理 205 | 206 | 将本地服务暴露在wsps上,供浏览器直接访问 207 | 208 | ### 暴露本地HTTP HTTPS服务 209 | 210 | 示意图如下: 211 | 212 | ```mermaid 213 | sequenceDiagram 214 | actor browser 215 | participant wsps 216 | participant wspc 217 | actor http as web server 218 | par register 219 | wspc --> wsps: websocket connect 220 | and connect 221 | browser ->> wsps : http request 222 | wsps ->> wspc : select wspc to serve 223 | wspc ->> http: open connect 224 | http ->> wspc: connect response 225 | wspc ->> wsps: connect response 226 | and reqeust 227 | wsps ->> wspc: http reqeust data 228 | wspc ->> http: http reqeust data 229 | http ->> wspc: http response data 230 | wspc ->> wsps: http response data 231 | wsps ->> browser: http response data 232 | end 233 | 234 | ``` 235 | 236 | 配置格式:`protocols://bind_address:port/[path]?mode=[mode]&value=[value]` 237 | 238 | - `protocols` 支持 http, https(支持websocket) 239 | - `bind_address`http服务地址 240 | - `port`http服务端口 241 | - `path`可选http服务路径 242 | - `mode`访问模式,为以下两种 243 | - `path` 路径模式 244 | - `domain` 域名模式 245 | 246 | 例: 247 | 248 | ```json 249 | { 250 | "client": [ 251 | { 252 | "server": "ws://mywsps.com:8010", 253 | "remote": [ 254 | "http://127.0.0.1:8080?mode=path&value=api", 255 | "http://127.0.0.1:8080/api/?mode=path&value=api", 256 | "http://127.0.0.1:8080?mode=domain&value=customwsp.com", 257 | "http://127.0.0.1:8080/api/?mode=domain&value=customapi.com" 258 | ] 259 | } 260 | ] 261 | } 262 | ``` 263 | 264 | 启动wspc, `./wsps -c wsps.json`,此时在wsps注册的访问映射关系由上至下为 265 | 266 | - 访问 http://mywsps.com:8010/api/greet -> http://127.0.0.1:8080/greet 267 | - 访问 http://mywsps.com:8010/api/greet -> http://127.0.0.1:8080/api/greet 268 | - 访问 http://customwsp.com:8010/api/greet -> http://127.0.0.1:8080/api/greet 269 | - 访问 http://customwsp.com:8010/greet -> http://127.0.0.1:8080/api/greet 270 | 271 | ### 包装本地TCP服务为websocket 272 | 273 | 某些场景下希望将TCP流量通过websocket进行转发,配置格式:`protocols://bind_address:port?mode=[mode]&value=[value]` 274 | 275 | - `protocols` 支持 tcp 276 | - `bind_address`服务IP地址 277 | - `port`服务端口 278 | - `mode`访问模式,为以下两种 279 | - `path` 路径模式 280 | - `domain` 域名模式 281 | 282 | 例暴露vnc服务在websocket,我们配置如下: 283 | 284 | ```json 285 | { 286 | "client": [ 287 | { 288 | "server": "ws://mywsps.com:8010", 289 | "remote": [ 290 | "tcp://127.0.0.1:5900?mode=path&value=vnc" 291 | ] 292 | } 293 | ] 294 | } 295 | ``` 296 | 297 | 这时我们可以打开[novnc](https://novnc.com/noVNC/vnc.html), 修改配置,修改为暴露的vnc服务参数,即可实现vnc的远程访问 298 | 299 | ## 网络穿透 300 | 301 | ### 普通模式 302 | 303 | 此模式下需要两台wspc配合wsps使用,三者功能角色如下: 304 | 305 | - wspc client 接入侧 306 | - wsps 中转侧,负责将wspc clinet的代理请求和数据中转给目标的wspc server 307 | - wspc server 服务侧,注册在wsps,负责打开来至wspc client的连接或转发数据 308 | 309 | 示意图如下 310 | 311 | ```mermaid 312 | sequenceDiagram 313 | actor client 314 | participant wspc1 315 | participant wsps 316 | participant wspc2 317 | actor target 318 | par target register 319 | wspc2 ->> wsps: websocket connect 320 | and local register 321 | wspc1 ->> wsps: websocket connect 322 | end 323 | par request connect 324 | client ->>+ wspc1: request 325 | wspc1 ->>+ wsps : request 326 | wsps ->>+ wspc2 : request 327 | wspc2 ->>+ target : request 328 | target ->>- wspc2 : reponse 329 | wspc2 ->>- wsps : reponse 330 | wsps ->>- wspc1 : reponse 331 | wspc1 ->>- client: reponse 332 | and tranfer data 333 | client -->> target: virtual connection, via wspc1 websocket 334 | target -->> client: virtual connection, via wspc2 websocket 335 | end 336 | ``` 337 | 338 | `wspc server`将本地服务注册至wsps等待`wspc client`连接,配置格式:`protocols://channel[:password]@[bind_address]:port` 339 | 340 | - `protocols` 支持 tcp 341 | - `channel`信道标识,注册在wsps上等待其他wspc接入的标识信息 342 | - `password`连接密码,接入的wspc连接密码需要一致才能通讯 343 | - `bind_address`监听地址 344 | - `port`服务端口 345 | 346 | 如注册本地网络中ssh服务配置如下 347 | 348 | ```json 349 | { 350 | "client": [ 351 | { 352 | "server": "ws://mywsps.com:8010", 353 | "remote": [ 354 | "tcp://ssh:ssh@192.168.1.200:22" 355 | ] 356 | } 357 | ] 358 | } 359 | ``` 360 | 361 | `wspc client`本地转发,开启本地端口来访问远程已注册的`wspc server`,配置格式:`protocols://remote_channel[:password]@[bind_address]:port` 362 | 363 | - `protocols` 支持 tcp 364 | - `channel`信道标识,wsps上已注册的的channel才能访问 365 | - `password`连接密码,与`RemoteForward`端密码一致才能通讯 366 | - `bind_address`监听地址 367 | - `port`本地端口 368 | 369 | 如访问已注册的`wspc server`ssh服务,本地`wspc client`进行如下配置 370 | 371 | ```json 372 | { 373 | "client": [ 374 | { 375 | "server": "ws://mywsps.com:8010", 376 | "local": [ 377 | "tcp://ssh:ssh@127.0.0.1:2200" 378 | ] 379 | } 380 | ] 381 | } 382 | ``` 383 | 384 | 此时访问本地的`127.0.0.1:2200`即为访问`wspc server`端中`192.168.1.200:22`的ssh服务 385 | 386 | ### vpn模式 387 | 388 | 此模式下需要两台wspc配合wsps使用,三者功能角色如下: 389 | 390 | - wspc client 代理侧 391 | - wsps 中转侧,负责将wspc clinet的代理请求和数据中转给目标的wspc server 392 | - wspc server 服务侧,注册在wsps,负责打开来至wspc client的代理连接转发代理数据 393 | 394 | `wspc server`端配置`tunnel://channel[:password]@` 395 | 396 | - `channel`信道标识,注册在wsps上等待其他wspc接入的标识信息 397 | - `password`连接密码,接入的wspc连接密码需要一致才能通讯 398 | 399 | 例: 400 | 401 | ```json 402 | { 403 | "client": [ 404 | { 405 | "server": "ws://mywsps.com:8010", 406 | "remote": [ 407 | "tunnel://work_tunnel:password@" 408 | ] 409 | } 410 | ] 411 | } 412 | ``` 413 | 414 | `wspc client`端配置`protocols://remote_channel[:password]@[bind_address]:port` 415 | 416 | - `protocols`代理协议,支持 socks5 代理,HTTP 代理 417 | - `remote_channel`信道标识,`RemoteForward`端注册的`channel` 418 | - `password`密码,对应`RemoteForward`端密码 419 | - `bind_address`可选,空地址表示监听所有网卡IP 420 | - `port`本地监听端口 421 | 422 | ```json 423 | { 424 | "client": [ 425 | { 426 | "server": "ws://mywsps.com:8010", 427 | "dynamic": [ 428 | "socks5://work_tunnel:password@127.0.0.1:1080" 429 | ] 430 | } 431 | ] 432 | } 433 | ``` 434 | 435 | 在`wspc client`端用socket5代理的连接和流量都会转发到`wspc server`端,如socket5代理下,访问`192.168.1.200:22`即访问`wspc server`端的`192.168.1.200:22` 436 | 437 | ## 日志记录 438 | 439 | wsp 支持日志分级记录,可在 wsps 或 wspc 中加入如下配置: 440 | 441 | ```json 442 | { 443 | "log": { 444 | "level": "info", 445 | "output": "/var/log/wsp.log" 446 | } 447 | } 448 | ``` 449 | 450 | 其中 `level` 支持以下级别: 451 | 452 | - error:只记录错误 453 | - info:记录信息,包含错误 454 | - debug:记录收发的信令,包含以上信息 455 | - trace:记录全过程信息 456 | 457 | ## 作为模块引入 458 | 459 | wsp在开发时考虑了与现有web服务的协作,支持作为一个功能模块引入 460 | 461 | ``` 462 | go get -u github.com/gowsp/wsp 463 | ``` 464 | 465 | 与官方http集成 466 | 467 | ```go 468 | import "github.com/gowsp/wsp/pkg/server" 469 | 470 | config := &server.Config{Auth: "auth"} 471 | 472 | server.NewWithHandler(config, http.NewServeMux()) 473 | server.NewWithHandler(config, http.DefaultServeMux) 474 | ``` 475 | 476 | 与gin集成 477 | 478 | ```go 479 | import "github.com/gowsp/wsp/pkg/server" 480 | 481 | config := &server.Config{Auth: "auth"} 482 | r := gin.Default() 483 | server.NewWithHandler(config, r) 484 | ``` 485 | 486 | ## TODO 487 | 488 | - [ ] 支持命令行模式使用 489 | 490 | ## 反馈建议 491 | 492 | 目前此项目为个人独立开发,难免会有BUG和功能设计上的缺陷,如有问题请提issues反馈,也欢迎参与代码或文档的贡献,祝使用愉快 493 | -------------------------------------------------------------------------------- /pkg/msg/msg.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.31.0 4 | // protoc v4.24.4 5 | // source: pkg/msg/msg.proto 6 | 7 | package msg 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type WspCmd int32 24 | 25 | const ( 26 | WspCmd_CONNECT WspCmd = 0 27 | WspCmd_RESPOND WspCmd = 1 28 | WspCmd_TRANSFER WspCmd = 2 29 | WspCmd_INTERRUPT WspCmd = 3 30 | ) 31 | 32 | // Enum value maps for WspCmd. 33 | var ( 34 | WspCmd_name = map[int32]string{ 35 | 0: "CONNECT", 36 | 1: "RESPOND", 37 | 2: "TRANSFER", 38 | 3: "INTERRUPT", 39 | } 40 | WspCmd_value = map[string]int32{ 41 | "CONNECT": 0, 42 | "RESPOND": 1, 43 | "TRANSFER": 2, 44 | "INTERRUPT": 3, 45 | } 46 | ) 47 | 48 | func (x WspCmd) Enum() *WspCmd { 49 | p := new(WspCmd) 50 | *p = x 51 | return p 52 | } 53 | 54 | func (x WspCmd) String() string { 55 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 56 | } 57 | 58 | func (WspCmd) Descriptor() protoreflect.EnumDescriptor { 59 | return file_pkg_msg_msg_proto_enumTypes[0].Descriptor() 60 | } 61 | 62 | func (WspCmd) Type() protoreflect.EnumType { 63 | return &file_pkg_msg_msg_proto_enumTypes[0] 64 | } 65 | 66 | func (x WspCmd) Number() protoreflect.EnumNumber { 67 | return protoreflect.EnumNumber(x) 68 | } 69 | 70 | // Deprecated: Use WspCmd.Descriptor instead. 71 | func (WspCmd) EnumDescriptor() ([]byte, []int) { 72 | return file_pkg_msg_msg_proto_rawDescGZIP(), []int{0} 73 | } 74 | 75 | type WspType int32 76 | 77 | const ( 78 | WspType_LOCAL WspType = 0 79 | WspType_REMOTE WspType = 1 80 | WspType_DYNAMIC WspType = 2 81 | ) 82 | 83 | // Enum value maps for WspType. 84 | var ( 85 | WspType_name = map[int32]string{ 86 | 0: "LOCAL", 87 | 1: "REMOTE", 88 | 2: "DYNAMIC", 89 | } 90 | WspType_value = map[string]int32{ 91 | "LOCAL": 0, 92 | "REMOTE": 1, 93 | "DYNAMIC": 2, 94 | } 95 | ) 96 | 97 | func (x WspType) Enum() *WspType { 98 | p := new(WspType) 99 | *p = x 100 | return p 101 | } 102 | 103 | func (x WspType) String() string { 104 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 105 | } 106 | 107 | func (WspType) Descriptor() protoreflect.EnumDescriptor { 108 | return file_pkg_msg_msg_proto_enumTypes[1].Descriptor() 109 | } 110 | 111 | func (WspType) Type() protoreflect.EnumType { 112 | return &file_pkg_msg_msg_proto_enumTypes[1] 113 | } 114 | 115 | func (x WspType) Number() protoreflect.EnumNumber { 116 | return protoreflect.EnumNumber(x) 117 | } 118 | 119 | // Deprecated: Use WspType.Descriptor instead. 120 | func (WspType) EnumDescriptor() ([]byte, []int) { 121 | return file_pkg_msg_msg_proto_rawDescGZIP(), []int{1} 122 | } 123 | 124 | type WspCode int32 125 | 126 | const ( 127 | WspCode_FAILED WspCode = 0 128 | WspCode_SUCCESS WspCode = 1 129 | ) 130 | 131 | // Enum value maps for WspCode. 132 | var ( 133 | WspCode_name = map[int32]string{ 134 | 0: "FAILED", 135 | 1: "SUCCESS", 136 | } 137 | WspCode_value = map[string]int32{ 138 | "FAILED": 0, 139 | "SUCCESS": 1, 140 | } 141 | ) 142 | 143 | func (x WspCode) Enum() *WspCode { 144 | p := new(WspCode) 145 | *p = x 146 | return p 147 | } 148 | 149 | func (x WspCode) String() string { 150 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 151 | } 152 | 153 | func (WspCode) Descriptor() protoreflect.EnumDescriptor { 154 | return file_pkg_msg_msg_proto_enumTypes[2].Descriptor() 155 | } 156 | 157 | func (WspCode) Type() protoreflect.EnumType { 158 | return &file_pkg_msg_msg_proto_enumTypes[2] 159 | } 160 | 161 | func (x WspCode) Number() protoreflect.EnumNumber { 162 | return protoreflect.EnumNumber(x) 163 | } 164 | 165 | // Deprecated: Use WspCode.Descriptor instead. 166 | func (WspCode) EnumDescriptor() ([]byte, []int) { 167 | return file_pkg_msg_msg_proto_rawDescGZIP(), []int{2} 168 | } 169 | 170 | type WspMessage struct { 171 | state protoimpl.MessageState 172 | sizeCache protoimpl.SizeCache 173 | unknownFields protoimpl.UnknownFields 174 | 175 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 176 | Cmd WspCmd `protobuf:"varint,2,opt,name=cmd,proto3,enum=WspCmd" json:"cmd,omitempty"` 177 | Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` 178 | } 179 | 180 | func (x *WspMessage) Reset() { 181 | *x = WspMessage{} 182 | if protoimpl.UnsafeEnabled { 183 | mi := &file_pkg_msg_msg_proto_msgTypes[0] 184 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 185 | ms.StoreMessageInfo(mi) 186 | } 187 | } 188 | 189 | func (x *WspMessage) String() string { 190 | return protoimpl.X.MessageStringOf(x) 191 | } 192 | 193 | func (*WspMessage) ProtoMessage() {} 194 | 195 | func (x *WspMessage) ProtoReflect() protoreflect.Message { 196 | mi := &file_pkg_msg_msg_proto_msgTypes[0] 197 | if protoimpl.UnsafeEnabled && x != nil { 198 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 199 | if ms.LoadMessageInfo() == nil { 200 | ms.StoreMessageInfo(mi) 201 | } 202 | return ms 203 | } 204 | return mi.MessageOf(x) 205 | } 206 | 207 | // Deprecated: Use WspMessage.ProtoReflect.Descriptor instead. 208 | func (*WspMessage) Descriptor() ([]byte, []int) { 209 | return file_pkg_msg_msg_proto_rawDescGZIP(), []int{0} 210 | } 211 | 212 | func (x *WspMessage) GetId() string { 213 | if x != nil { 214 | return x.Id 215 | } 216 | return "" 217 | } 218 | 219 | func (x *WspMessage) GetCmd() WspCmd { 220 | if x != nil { 221 | return x.Cmd 222 | } 223 | return WspCmd_CONNECT 224 | } 225 | 226 | func (x *WspMessage) GetData() []byte { 227 | if x != nil { 228 | return x.Data 229 | } 230 | return nil 231 | } 232 | 233 | type WspRequest struct { 234 | state protoimpl.MessageState 235 | sizeCache protoimpl.SizeCache 236 | unknownFields protoimpl.UnknownFields 237 | 238 | Type WspType `protobuf:"varint,1,opt,name=type,proto3,enum=WspType" json:"type,omitempty"` 239 | Data string `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` 240 | } 241 | 242 | func (x *WspRequest) Reset() { 243 | *x = WspRequest{} 244 | if protoimpl.UnsafeEnabled { 245 | mi := &file_pkg_msg_msg_proto_msgTypes[1] 246 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 247 | ms.StoreMessageInfo(mi) 248 | } 249 | } 250 | 251 | func (x *WspRequest) String() string { 252 | return protoimpl.X.MessageStringOf(x) 253 | } 254 | 255 | func (*WspRequest) ProtoMessage() {} 256 | 257 | func (x *WspRequest) ProtoReflect() protoreflect.Message { 258 | mi := &file_pkg_msg_msg_proto_msgTypes[1] 259 | if protoimpl.UnsafeEnabled && x != nil { 260 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 261 | if ms.LoadMessageInfo() == nil { 262 | ms.StoreMessageInfo(mi) 263 | } 264 | return ms 265 | } 266 | return mi.MessageOf(x) 267 | } 268 | 269 | // Deprecated: Use WspRequest.ProtoReflect.Descriptor instead. 270 | func (*WspRequest) Descriptor() ([]byte, []int) { 271 | return file_pkg_msg_msg_proto_rawDescGZIP(), []int{1} 272 | } 273 | 274 | func (x *WspRequest) GetType() WspType { 275 | if x != nil { 276 | return x.Type 277 | } 278 | return WspType_LOCAL 279 | } 280 | 281 | func (x *WspRequest) GetData() string { 282 | if x != nil { 283 | return x.Data 284 | } 285 | return "" 286 | } 287 | 288 | type WspResponse struct { 289 | state protoimpl.MessageState 290 | sizeCache protoimpl.SizeCache 291 | unknownFields protoimpl.UnknownFields 292 | 293 | Code WspCode `protobuf:"varint,1,opt,name=code,proto3,enum=WspCode" json:"code,omitempty"` 294 | Data string `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` 295 | } 296 | 297 | func (x *WspResponse) Reset() { 298 | *x = WspResponse{} 299 | if protoimpl.UnsafeEnabled { 300 | mi := &file_pkg_msg_msg_proto_msgTypes[2] 301 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 302 | ms.StoreMessageInfo(mi) 303 | } 304 | } 305 | 306 | func (x *WspResponse) String() string { 307 | return protoimpl.X.MessageStringOf(x) 308 | } 309 | 310 | func (*WspResponse) ProtoMessage() {} 311 | 312 | func (x *WspResponse) ProtoReflect() protoreflect.Message { 313 | mi := &file_pkg_msg_msg_proto_msgTypes[2] 314 | if protoimpl.UnsafeEnabled && x != nil { 315 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 316 | if ms.LoadMessageInfo() == nil { 317 | ms.StoreMessageInfo(mi) 318 | } 319 | return ms 320 | } 321 | return mi.MessageOf(x) 322 | } 323 | 324 | // Deprecated: Use WspResponse.ProtoReflect.Descriptor instead. 325 | func (*WspResponse) Descriptor() ([]byte, []int) { 326 | return file_pkg_msg_msg_proto_rawDescGZIP(), []int{2} 327 | } 328 | 329 | func (x *WspResponse) GetCode() WspCode { 330 | if x != nil { 331 | return x.Code 332 | } 333 | return WspCode_FAILED 334 | } 335 | 336 | func (x *WspResponse) GetData() string { 337 | if x != nil { 338 | return x.Data 339 | } 340 | return "" 341 | } 342 | 343 | var File_pkg_msg_msg_proto protoreflect.FileDescriptor 344 | 345 | var file_pkg_msg_msg_proto_rawDesc = []byte{ 346 | 0x0a, 0x11, 0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x73, 0x67, 0x2f, 0x6d, 0x73, 0x67, 0x2e, 0x70, 0x72, 347 | 0x6f, 0x74, 0x6f, 0x22, 0x4b, 0x0a, 0x0a, 0x57, 0x73, 0x70, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 348 | 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 349 | 0x64, 0x12, 0x19, 0x0a, 0x03, 0x63, 0x6d, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x07, 350 | 0x2e, 0x57, 0x73, 0x70, 0x43, 0x6d, 0x64, 0x52, 0x03, 0x63, 0x6d, 0x64, 0x12, 0x12, 0x0a, 0x04, 351 | 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 352 | 0x22, 0x3e, 0x0a, 0x0a, 0x57, 0x73, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 353 | 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x08, 0x2e, 0x57, 354 | 0x73, 0x70, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 355 | 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 356 | 0x22, 0x3f, 0x0a, 0x0b, 0x57, 0x73, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 357 | 0x1c, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x08, 0x2e, 358 | 0x57, 0x73, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x12, 0x0a, 359 | 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 360 | 0x61, 0x2a, 0x3f, 0x0a, 0x06, 0x57, 0x73, 0x70, 0x43, 0x6d, 0x64, 0x12, 0x0b, 0x0a, 0x07, 0x43, 361 | 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x53, 0x50, 362 | 0x4f, 0x4e, 0x44, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 363 | 0x52, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x52, 0x55, 0x50, 0x54, 364 | 0x10, 0x03, 0x2a, 0x2d, 0x0a, 0x07, 0x57, 0x73, 0x70, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 365 | 0x05, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x52, 0x45, 0x4d, 0x4f, 366 | 0x54, 0x45, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x59, 0x4e, 0x41, 0x4d, 0x49, 0x43, 0x10, 367 | 0x02, 0x2a, 0x22, 0x0a, 0x07, 0x57, 0x73, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x0a, 0x0a, 0x06, 368 | 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x43, 0x43, 369 | 0x45, 0x53, 0x53, 0x10, 0x01, 0x42, 0x1e, 0x5a, 0x1c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 370 | 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x77, 0x73, 0x70, 0x2f, 0x77, 0x73, 0x70, 0x2f, 0x70, 0x6b, 371 | 0x67, 0x2f, 0x6d, 0x73, 0x67, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 372 | } 373 | 374 | var ( 375 | file_pkg_msg_msg_proto_rawDescOnce sync.Once 376 | file_pkg_msg_msg_proto_rawDescData = file_pkg_msg_msg_proto_rawDesc 377 | ) 378 | 379 | func file_pkg_msg_msg_proto_rawDescGZIP() []byte { 380 | file_pkg_msg_msg_proto_rawDescOnce.Do(func() { 381 | file_pkg_msg_msg_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_msg_msg_proto_rawDescData) 382 | }) 383 | return file_pkg_msg_msg_proto_rawDescData 384 | } 385 | 386 | var file_pkg_msg_msg_proto_enumTypes = make([]protoimpl.EnumInfo, 3) 387 | var file_pkg_msg_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 388 | var file_pkg_msg_msg_proto_goTypes = []interface{}{ 389 | (WspCmd)(0), // 0: WspCmd 390 | (WspType)(0), // 1: WspType 391 | (WspCode)(0), // 2: WspCode 392 | (*WspMessage)(nil), // 3: WspMessage 393 | (*WspRequest)(nil), // 4: WspRequest 394 | (*WspResponse)(nil), // 5: WspResponse 395 | } 396 | var file_pkg_msg_msg_proto_depIdxs = []int32{ 397 | 0, // 0: WspMessage.cmd:type_name -> WspCmd 398 | 1, // 1: WspRequest.type:type_name -> WspType 399 | 2, // 2: WspResponse.code:type_name -> WspCode 400 | 3, // [3:3] is the sub-list for method output_type 401 | 3, // [3:3] is the sub-list for method input_type 402 | 3, // [3:3] is the sub-list for extension type_name 403 | 3, // [3:3] is the sub-list for extension extendee 404 | 0, // [0:3] is the sub-list for field type_name 405 | } 406 | 407 | func init() { file_pkg_msg_msg_proto_init() } 408 | func file_pkg_msg_msg_proto_init() { 409 | if File_pkg_msg_msg_proto != nil { 410 | return 411 | } 412 | if !protoimpl.UnsafeEnabled { 413 | file_pkg_msg_msg_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 414 | switch v := v.(*WspMessage); i { 415 | case 0: 416 | return &v.state 417 | case 1: 418 | return &v.sizeCache 419 | case 2: 420 | return &v.unknownFields 421 | default: 422 | return nil 423 | } 424 | } 425 | file_pkg_msg_msg_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 426 | switch v := v.(*WspRequest); i { 427 | case 0: 428 | return &v.state 429 | case 1: 430 | return &v.sizeCache 431 | case 2: 432 | return &v.unknownFields 433 | default: 434 | return nil 435 | } 436 | } 437 | file_pkg_msg_msg_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 438 | switch v := v.(*WspResponse); i { 439 | case 0: 440 | return &v.state 441 | case 1: 442 | return &v.sizeCache 443 | case 2: 444 | return &v.unknownFields 445 | default: 446 | return nil 447 | } 448 | } 449 | } 450 | type x struct{} 451 | out := protoimpl.TypeBuilder{ 452 | File: protoimpl.DescBuilder{ 453 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 454 | RawDescriptor: file_pkg_msg_msg_proto_rawDesc, 455 | NumEnums: 3, 456 | NumMessages: 3, 457 | NumExtensions: 0, 458 | NumServices: 0, 459 | }, 460 | GoTypes: file_pkg_msg_msg_proto_goTypes, 461 | DependencyIndexes: file_pkg_msg_msg_proto_depIdxs, 462 | EnumInfos: file_pkg_msg_msg_proto_enumTypes, 463 | MessageInfos: file_pkg_msg_msg_proto_msgTypes, 464 | }.Build() 465 | File_pkg_msg_msg_proto = out.File 466 | file_pkg_msg_msg_proto_rawDesc = nil 467 | file_pkg_msg_msg_proto_goTypes = nil 468 | file_pkg_msg_msg_proto_depIdxs = nil 469 | } 470 | --------------------------------------------------------------------------------