diff --git a/internal/pinggy/README.md b/internal/pinggy/README.md new file mode 100644 index 00000000..cd270021 --- /dev/null +++ b/internal/pinggy/README.md @@ -0,0 +1,54 @@ +# Pinggy + +[Pinggy](https://pinggy.io/) - nice service for public tunnels to your local services. + +**Features:** + +- A free account does not require registration. +- It does not require downloading third-party binaries and works over the SSH protocol. +- Works with HTTP, TCP and UDP protocols. +- Creates HTTPS for your HTTP services. + +> [!IMPORTANT] +> A free account creates a tunnel with a random address that only works for an hour. It is suitable for testing purposes ONLY. + +> [!CAUTION] +> Public access to go2rtc without authorization puts your entire home network at risk. Use with caution. + +**Why:** + +- It's easy to set up HTTPS for testing two-way audio. +- It's easy to check whether external access via WebRTC technology will work. +- It's easy to share direct access to your RTSP or HTTP camera with the go2rtc developer. If such access is necessary to debug your problem. + +## Configuration + +You will find public links in the go2rtc log after startup. + +**Tunnel to go2rtc WebUI.** + +```yaml +pinggy: + tunnel: http://localhost:1984 +``` + +**Tunnel to RTSP camera.** + +For example, you have camera: `rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0` + +```yaml +pinggy: + tunnel: tcp://192.168.10.91:554 +``` + +In go2rtc logs you will get similar output: + +``` +16:17:43.167 INF [pinggy] proxy url=tcp://abcde-123-123-123-123.a.free.pinggy.link:12345 +``` + +Now you have working stream: + +``` +rtsp://admin:password@abcde-123-123-123-123.a.free.pinggy.link:12345/cam/realmonitor?channel=1&subtype=0 +``` diff --git a/internal/pinggy/pinggy.go b/internal/pinggy/pinggy.go new file mode 100644 index 00000000..2e7258e2 --- /dev/null +++ b/internal/pinggy/pinggy.go @@ -0,0 +1,60 @@ +package pinggy + +import ( + "net/url" + + "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/pkg/pinggy" + "github.com/rs/zerolog" +) + +func Init() { + var cfg struct { + Mod struct { + Tunnel string `yaml:"tunnel"` + } `yaml:"pinggy"` + } + + app.LoadConfig(&cfg) + + if cfg.Mod.Tunnel == "" { + return + } + + log = app.GetLogger("pinggy") + + u, err := url.Parse(cfg.Mod.Tunnel) + if err != nil { + log.Error().Err(err).Send() + return + } + + go proxy(u.Scheme, u.Host) +} + +var log zerolog.Logger + +func proxy(proto, address string) { + client, err := pinggy.NewClient(proto) + if err != nil { + log.Error().Err(err).Send() + return + } + defer client.Close() + + urls, err := client.GetURLs() + if err != nil { + log.Error().Err(err).Send() + return + } + + for _, s := range urls { + log.Info().Str("url", s).Msgf("[pinggy] proxy") + } + + err = client.Proxy(address) + if err != nil { + log.Error().Err(err).Send() + return + } +} diff --git a/main.go b/main.go index 95e59ddd..c548e99f 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/nest" "github.com/AlexxIT/go2rtc/internal/ngrok" "github.com/AlexxIT/go2rtc/internal/onvif" + "github.com/AlexxIT/go2rtc/internal/pinggy" "github.com/AlexxIT/go2rtc/internal/ring" "github.com/AlexxIT/go2rtc/internal/roborock" "github.com/AlexxIT/go2rtc/internal/rtmp" @@ -99,6 +100,7 @@ func main() { // Helper modules {"debug", debug.Init}, {"ngrok", ngrok.Init}, + {"pinggy", pinggy.Init}, {"srtp", srtp.Init}, } diff --git a/pkg/pinggy/pinggy.go b/pkg/pinggy/pinggy.go new file mode 100644 index 00000000..22ebfc91 --- /dev/null +++ b/pkg/pinggy/pinggy.go @@ -0,0 +1,137 @@ +package pinggy + +import ( + "context" + "encoding/json" + "errors" + "io" + "net" + "net/http" + "time" + + "golang.org/x/crypto/ssh" +) + +type Client struct { + SSH *ssh.Client + TCP net.Listener + API *http.Client +} + +func NewClient(proto string) (*Client, error) { + switch proto { + case "http", "tcp", "tls", "tlstcp": + case "": + proto = "http" + default: + return nil, errors.New("pinggy: unsupported proto: " + proto) + } + + config := &ssh.ClientConfig{ + User: "auth+" + proto, + Auth: []ssh.AuthMethod{ssh.Password("nopass")}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 5 * time.Second, + } + + client, err := ssh.Dial("tcp", "a.pinggy.io:443", config) + if err != nil { + return nil, err + } + + ln, err := client.Listen("tcp", "0.0.0.0:0") + if err != nil { + _ = client.Close() + return nil, err + } + + c := &Client{ + SSH: client, + TCP: ln, + API: &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return client.Dial(network, addr) + }, + }, + }, + } + + if proto == "http" { + if err = c.NewSession(); err != nil { + _ = client.Close() + return nil, err + } + } + + return c, nil +} + +func (c *Client) Close() error { + return errors.Join(c.SSH.Close(), c.TCP.Close()) +} + +func (c *Client) NewSession() error { + session, err := c.SSH.NewSession() + if err != nil { + return err + } + return session.Shell() +} + +func (c *Client) GetURLs() ([]string, error) { + res, err := c.API.Get("http://localhost:4300/urls") + if err != nil { + return nil, err + } + defer res.Body.Close() + + var v struct { + URLs []string `json:"urls"` + } + + if err = json.NewDecoder(res.Body).Decode(&v); err != nil { + return nil, err + } + + return v.URLs, nil +} + +func (c *Client) Proxy(address string) error { + defer c.TCP.Close() + + for { + conn, err := c.TCP.Accept() + if err != nil { + return err + } + go proxy(conn, address) + } +} + +func proxy(conn1 net.Conn, address string) { + defer conn1.Close() + + conn2, err := net.Dial("tcp", address) + if err != nil { + return + } + defer conn2.Close() + + go io.Copy(conn2, conn1) + io.Copy(conn1, conn2) +} + +// DialTLS like ssh.Dial but with TLS +//func DialTLS(network, addr, sni string, config *ssh.ClientConfig) (*ssh.Client, error) { +// conn, err := net.DialTimeout(network, addr, config.Timeout) +// if err != nil { +// return nil, err +// } +// conn = tls.Client(conn, &tls.Config{ServerName: sni, InsecureSkipVerify: sni == ""}) +// c, chans, reqs, err := ssh.NewClientConn(conn, addr, config) +// if err != nil { +// return nil, err +// } +// return ssh.NewClient(c, chans, reqs), nil +//}