Skip to content

Commit 4934fa4

Browse files
committed
Add support tunnels via Pinggy #1853
1 parent 248fc7a commit 4934fa4

File tree

4 files changed

+253
-0
lines changed

4 files changed

+253
-0
lines changed

internal/pinggy/README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Pinggy
2+
3+
[Pinggy](https://pinggy.io/) - nice service for public tunnels to your local services.
4+
5+
**Features:**
6+
7+
- A free account does not require registration.
8+
- It does not require downloading third-party binaries and works over the SSH protocol.
9+
- Works with HTTP, TCP and UDP protocols.
10+
- Creates HTTPS for your HTTP services.
11+
12+
> [!IMPORTANT]
13+
> A free account creates a tunnel with a random address that only works for an hour. It is suitable for testing purposes ONLY.
14+
15+
> [!CAUTION]
16+
> Public access to go2rtc without authorization puts your entire home network at risk. Use with caution.
17+
18+
**Why:**
19+
20+
- It's easy to set up HTTPS for testing two-way audio.
21+
- It's easy to check whether external access via WebRTC technology will work.
22+
- 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.
23+
24+
## Configuration
25+
26+
You will find public links in the go2rtc log after startup.
27+
28+
**Tunnel to go2rtc WebUI.**
29+
30+
```yaml
31+
pinggy:
32+
tunnel: http://localhost:1984
33+
```
34+
35+
**Tunnel to RTSP camera.**
36+
37+
For example, you have camera: `rtsp://admin:[email protected]/cam/realmonitor?channel=1&subtype=0`
38+
39+
```yaml
40+
pinggy:
41+
tunnel: tcp://192.168.10.91:554
42+
```
43+
44+
In go2rtc logs you will get similar output:
45+
46+
```
47+
16:17:43.167 INF [pinggy] proxy url=tcp://abcde-123-123-123-123.a.free.pinggy.link:12345
48+
```
49+
50+
Now you have working stream:
51+
52+
```
53+
rtsp://admin:[email protected]:12345/cam/realmonitor?channel=1&subtype=0
54+
```

internal/pinggy/pinggy.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package pinggy
2+
3+
import (
4+
"net/url"
5+
6+
"github.com/AlexxIT/go2rtc/internal/app"
7+
"github.com/AlexxIT/go2rtc/pkg/pinggy"
8+
"github.com/rs/zerolog"
9+
)
10+
11+
func Init() {
12+
var cfg struct {
13+
Mod struct {
14+
Tunnel string `yaml:"tunnel"`
15+
} `yaml:"pinggy"`
16+
}
17+
18+
app.LoadConfig(&cfg)
19+
20+
if cfg.Mod.Tunnel == "" {
21+
return
22+
}
23+
24+
log = app.GetLogger("pinggy")
25+
26+
u, err := url.Parse(cfg.Mod.Tunnel)
27+
if err != nil {
28+
log.Error().Err(err).Send()
29+
return
30+
}
31+
32+
go proxy(u.Scheme, u.Host)
33+
}
34+
35+
var log zerolog.Logger
36+
37+
func proxy(proto, address string) {
38+
client, err := pinggy.NewClient(proto)
39+
if err != nil {
40+
log.Error().Err(err).Send()
41+
return
42+
}
43+
defer client.Close()
44+
45+
urls, err := client.GetURLs()
46+
if err != nil {
47+
log.Error().Err(err).Send()
48+
return
49+
}
50+
51+
for _, s := range urls {
52+
log.Info().Str("url", s).Msgf("[pinggy] proxy")
53+
}
54+
55+
err = client.Proxy(address)
56+
if err != nil {
57+
log.Error().Err(err).Send()
58+
return
59+
}
60+
}

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/AlexxIT/go2rtc/internal/nest"
3131
"github.com/AlexxIT/go2rtc/internal/ngrok"
3232
"github.com/AlexxIT/go2rtc/internal/onvif"
33+
"github.com/AlexxIT/go2rtc/internal/pinggy"
3334
"github.com/AlexxIT/go2rtc/internal/ring"
3435
"github.com/AlexxIT/go2rtc/internal/roborock"
3536
"github.com/AlexxIT/go2rtc/internal/rtmp"
@@ -99,6 +100,7 @@ func main() {
99100
// Helper modules
100101
{"debug", debug.Init},
101102
{"ngrok", ngrok.Init},
103+
{"pinggy", pinggy.Init},
102104
{"srtp", srtp.Init},
103105
}
104106

pkg/pinggy/pinggy.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package pinggy
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"io"
8+
"net"
9+
"net/http"
10+
"time"
11+
12+
"golang.org/x/crypto/ssh"
13+
)
14+
15+
type Client struct {
16+
SSH *ssh.Client
17+
TCP net.Listener
18+
API *http.Client
19+
}
20+
21+
func NewClient(proto string) (*Client, error) {
22+
switch proto {
23+
case "http", "tcp", "tls", "tlstcp":
24+
case "":
25+
proto = "http"
26+
default:
27+
return nil, errors.New("pinggy: unsupported proto: " + proto)
28+
}
29+
30+
config := &ssh.ClientConfig{
31+
User: "auth+" + proto,
32+
Auth: []ssh.AuthMethod{ssh.Password("nopass")},
33+
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
34+
Timeout: 5 * time.Second,
35+
}
36+
37+
client, err := ssh.Dial("tcp", "a.pinggy.io:443", config)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
ln, err := client.Listen("tcp", "0.0.0.0:0")
43+
if err != nil {
44+
_ = client.Close()
45+
return nil, err
46+
}
47+
48+
c := &Client{
49+
SSH: client,
50+
TCP: ln,
51+
API: &http.Client{
52+
Transport: &http.Transport{
53+
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
54+
return client.Dial(network, addr)
55+
},
56+
},
57+
},
58+
}
59+
60+
if proto == "http" {
61+
if err = c.NewSession(); err != nil {
62+
_ = client.Close()
63+
return nil, err
64+
}
65+
}
66+
67+
return c, nil
68+
}
69+
70+
func (c *Client) Close() error {
71+
return errors.Join(c.SSH.Close(), c.TCP.Close())
72+
}
73+
74+
func (c *Client) NewSession() error {
75+
session, err := c.SSH.NewSession()
76+
if err != nil {
77+
return err
78+
}
79+
return session.Shell()
80+
}
81+
82+
func (c *Client) GetURLs() ([]string, error) {
83+
res, err := c.API.Get("http://localhost:4300/urls")
84+
if err != nil {
85+
return nil, err
86+
}
87+
defer res.Body.Close()
88+
89+
var v struct {
90+
URLs []string `json:"urls"`
91+
}
92+
93+
if err = json.NewDecoder(res.Body).Decode(&v); err != nil {
94+
return nil, err
95+
}
96+
97+
return v.URLs, nil
98+
}
99+
100+
func (c *Client) Proxy(address string) error {
101+
defer c.TCP.Close()
102+
103+
for {
104+
conn, err := c.TCP.Accept()
105+
if err != nil {
106+
return err
107+
}
108+
go proxy(conn, address)
109+
}
110+
}
111+
112+
func proxy(conn1 net.Conn, address string) {
113+
defer conn1.Close()
114+
115+
conn2, err := net.Dial("tcp", address)
116+
if err != nil {
117+
return
118+
}
119+
defer conn2.Close()
120+
121+
go io.Copy(conn2, conn1)
122+
io.Copy(conn1, conn2)
123+
}
124+
125+
// DialTLS like ssh.Dial but with TLS
126+
//func DialTLS(network, addr, sni string, config *ssh.ClientConfig) (*ssh.Client, error) {
127+
// conn, err := net.DialTimeout(network, addr, config.Timeout)
128+
// if err != nil {
129+
// return nil, err
130+
// }
131+
// conn = tls.Client(conn, &tls.Config{ServerName: sni, InsecureSkipVerify: sni == ""})
132+
// c, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
133+
// if err != nil {
134+
// return nil, err
135+
// }
136+
// return ssh.NewClient(c, chans, reqs), nil
137+
//}

0 commit comments

Comments
 (0)