Skip to content

Commit efd1c6b

Browse files
authored
support concurent remote connetion (#20)
* support concurent remote connetion the client now open a multiplexed connection to the server, does the handshake then start listening for incoming connection from the server. This allow to received multiple incoming connection concurently and handle them properly. Everytime the client receive a new connection it open one to the local app too. * fix tests * fix race condition during server starup * closes destination tcp stream once copy is done * update readme
1 parent 75d0277 commit efd1c6b

File tree

10 files changed

+301
-137
lines changed

10 files changed

+301
-137
lines changed

README.md

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,51 +2,50 @@
22

33
A down to earth tcp router based on traefik tcp streaming and supports multiple backends using [valkyrie](https://github.com/abronan/valkeyrie)
44

5-
65
## Build
76

87
```
9-
git clone https://github.com/xmonader/tcprouter
10-
make all
8+
git clone https://github.com/threefoldtech/tcprouter
9+
make
1110
```
11+
1212
This will generate two binaries in bin dir
1313

1414
- `trs`: tcp router server
1515
- `trc`: tcp router client
1616

17-
1817
## Running
1918

20-
configfile: router.toml
19+
Example configuration file
20+
2121
```toml
2222
[server]
23-
addr = "0.0.0.0"
24-
port = 443
25-
httpport = 80
23+
addr = "0.0.0.0" # listening address for all entrypoints (http, https, tcp router client)
24+
port = 443 # TLS listening port
25+
httpport = 80 # HTTP listening port
2626

27-
[server.dbbackend]
27+
[server.dbbackend] # configuration for the redis backend
2828
type = "redis"
2929
addr = "127.0.0.1"
3030
port = 6379
31-
refresh = 10
31+
refresh = 10 # make the tcp router poll for new configuration every 10 seconds
32+
3233

3334
[server.services]
34-
[server.services."www.google.com"]
35+
[server.services."mydomain.com"]
3536
addr = "172.217.19.46"
3637
tlsport = 443
3738
httpport = 80
3839
```
39-
then
40-
`./tcprouter router.toml`
4140

41+
then `trs -config router.toml`
4242

4343
Please notice if you are using low numbered port like 80 or 443 you can use sudo or setcap before running the binary.
44-
- `sudo ./tcprouter router.toml`
45-
- setcap: `sudo setcap CAP_NET_BIND_SERVICE=+eip PATH_TO_TCPROUTER`
44+
- `sudo setcap CAP_NET_BIND_SERVICE=+eip trs`
4645

4746
### router.toml
4847

49-
We have two toml sections so far
48+
We have two 3 sections so far
5049

5150
#### [server]
5251

@@ -71,6 +70,19 @@ refresh = 10
7170

7271
in `server.dbbackend` we define the backend kv store and its connection information `addr,port` and how often we want to reload the data from the kv store using `refresh` key in seconds.
7372

73+
#### [server.services]
74+
75+
```toml
76+
[server.services]
77+
[server.services."mydomain.com"]
78+
addr = "172.217.19.46"
79+
tlsport = 443
80+
httpport = 80
81+
```
82+
83+
Services are static configuration that are hardcoded in the configuration file instead of coming from the database backend.
84+
In this example the request for domain `mydomain.com` will be forwarded to the backend server at `172.217.19.46:443` for TLS traffic and `172.217.19.46:80` for non TLS traffic.
85+
7486
## Data representation in KV
7587

7688
```shell
@@ -159,7 +171,7 @@ func main() {
159171

160172
### Python
161173

162-
```python3
174+
```python
163175
import base64
164176
import json
165177
import redis
@@ -195,3 +207,28 @@ So your browser go to your `127.0.0.1:443` on requesting google or bing.
195207
to add a global `catch all` service
196208

197209
`python3 create_service.py CATCH_ALL 'CATCH_ALL' '127.0.0.1:9092'`
210+
211+
## Reverse tunneling
212+
213+
TCP router also support to forward connection to a server that is hidden behind NAT. The way it works is on the hidden client side,
214+
a small client runs and opens a connection to the tcp router server. The client sends a secret during an handshake with the server to authenticate the connection.
215+
216+
The server then keeps the connection opens and is able to forward incoming public traffic to the open connection. This is specially useful if there is no way for the tcp router server to open a connection to the backend. Usually because of NAT.
217+
218+
![reverse_tunnel](reverse_tunnel.png)
219+
220+
### example
221+
222+
Fist create the configuration on the server side. The only required field in the configuration is the secret for the client connection:
223+
224+
```toml
225+
[server.services]
226+
[server.services."mydomain.com"]
227+
clientsecret = "TB2pbZ5FR8GQZp9W2z97jBjxSgWgQKaQTxEgrZNBa4pEFzv3PJcRVEtG2a5BU9qd"
228+
```
229+
230+
Second starts the tcp router client and make it opens a connection to the tcp router server:
231+
The following command will connect the the server located at `tcprouter-1.com`, forward traffic for `mydomain.com` to the local application running at `localhost:8080` and send the response back.
232+
233+
234+
`trc -local localhost:8080 -remote tcprouter-1.com -secret TB2pbZ5FR8GQZp9W2z97jBjxSgWgQKaQTxEgrZNBa4pEFzv3PJcRVEtG2a5BU9qd`

client.go

Lines changed: 133 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,190 @@
11
package tcprouter
22

33
import (
4+
"context"
45
"fmt"
56
"io"
67
"net"
78

9+
"github.com/libp2p/go-yamux"
810
"github.com/rs/zerolog/log"
911
)
1012

1113
type Client struct {
12-
// connection to the tcp router server
13-
RemoteConn net.Conn
14-
// connection to the local application
15-
LocalConn net.Conn
16-
14+
localAddr string
15+
remoteAddr string
1716
// secret used to identify the connection in the tcp router server
1817
secret []byte
18+
19+
// connection to the tcp router server
20+
remoteSession *yamux.Session
1921
}
2022

2123
// NewClient creates a new TCP router client
22-
func NewClient(secret string) *Client {
24+
func NewClient(secret, local, remote string) *Client {
2325
return &Client{
24-
secret: []byte(secret),
26+
localAddr: local,
27+
remoteAddr: remote,
28+
secret: []byte(secret),
29+
}
30+
}
31+
32+
// Start starts the client by opening a connection to the router server, doing the handshake
33+
// then start listening for incoming steam from the router server
34+
func (c Client) Start(ctx context.Context) error {
35+
if err := c.connectRemote(c.remoteAddr); err != nil {
36+
return fmt.Errorf("failed to connect to TCP router server: %w", err)
2537
}
38+
39+
log.Info().Msg("start handshake")
40+
if err := c.handshake(); err != nil {
41+
return fmt.Errorf("failed to handshake with TCP router server: %w", err)
42+
}
43+
log.Info().Msg("handshake done")
44+
45+
return c.listen(ctx)
2646
}
2747

28-
func (c *Client) ConnectRemote(addr string) error {
48+
func (c *Client) connectRemote(addr string) error {
2949
if len(c.secret) == 0 {
3050
return fmt.Errorf("no secret configured")
3151
}
3252

33-
conn, err := net.Dial("tcp", addr)
53+
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
3454
if err != nil {
3555
return err
3656
}
3757

38-
c.RemoteConn = conn
58+
conn, err := net.DialTCP("tcp", nil, tcpAddr)
59+
if err != nil {
60+
return err
61+
}
62+
63+
// Setup client side of yamux
64+
session, err := yamux.Client(conn, nil)
65+
if err != nil {
66+
panic(err)
67+
}
68+
69+
c.remoteSession = session
3970

4071
return nil
4172
}
4273

43-
func (c *Client) ConnectLocal(addr string) error {
44-
conn, err := net.Dial("tcp", addr)
74+
func (c *Client) connectLocal(addr string) (WriteCloser, error) {
75+
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
4576
if err != nil {
46-
return err
77+
return nil, err
4778
}
4879

49-
c.LocalConn = conn
80+
conn, err := net.DialTCP("tcp", nil, tcpAddr)
81+
if err != nil {
82+
return nil, err
83+
}
5084

51-
return nil
85+
return conn, nil
5286
}
5387

54-
func (c *Client) Handshake() error {
55-
if c.RemoteConn == nil {
88+
func (c *Client) handshake() error {
89+
if c.remoteSession == nil {
5690
return fmt.Errorf("not connected")
5791
}
5892

5993
h := Handshake{
6094
MagicNr: MagicNr,
6195
Secret: []byte(c.secret),
6296
}
63-
// at this point if the server refuse the hanshake it will
97+
// at this point if the server refuse the handshake it will
6498
// just close the connection which should return an error
65-
return h.Write(c.RemoteConn)
99+
stream, err := c.remoteSession.OpenStream()
100+
if err != nil {
101+
return err
102+
}
103+
defer stream.Close()
104+
105+
return h.Write(stream)
66106
}
67107

68-
func (c *Client) Forward() {
108+
func (c *Client) listen(ctx context.Context) error {
109+
ctx, cancel := context.WithCancel(ctx)
110+
defer cancel()
69111

112+
cCon := make(chan WriteCloser)
70113
cErr := make(chan error)
71-
defer func() {
72-
c.RemoteConn.Close()
73-
c.LocalConn.Close()
74-
}()
75-
76-
go forward(c.LocalConn, c.RemoteConn, cErr)
77-
go forward(c.RemoteConn, c.LocalConn, cErr)
78-
79-
err := <-cErr
80-
if err != nil {
81-
log.Error().Err(err).Msg("Error during connection")
114+
go func(ctx context.Context, cCon chan<- WriteCloser, cErr chan<- error) {
115+
for {
116+
select {
117+
case <-ctx.Done():
118+
return
119+
default:
120+
conn, err := c.remoteSession.AcceptStream()
121+
if err != nil {
122+
cErr <- err
123+
return
124+
}
125+
cCon <- WrapConn(conn)
126+
}
127+
}
128+
}(ctx, cCon, cErr)
129+
130+
for {
131+
select {
132+
case <-ctx.Done():
133+
return nil
134+
case err := <-cErr:
135+
return fmt.Errorf("accept connection failed: %w", err)
136+
case remote := <-cCon:
137+
log.Info().
138+
Str("remote add", remote.RemoteAddr().String()).
139+
Msg("incoming stream, connect to local application")
140+
141+
local, err := c.connectLocal(c.localAddr)
142+
if err != nil {
143+
return fmt.Errorf("failed to connect to local application: %w", err)
144+
}
145+
146+
go func(remote, local WriteCloser) {
147+
log.Info().Msg("start forwarding")
148+
149+
cErr := make(chan error)
150+
go forward(local, remote, cErr)
151+
go forward(remote, local, cErr)
152+
153+
err = <-cErr
154+
if err != nil {
155+
log.Error().Err(err).Msg("Error during forwarding: %w")
156+
}
157+
158+
<-cErr
159+
160+
if err := remote.Close(); err != nil {
161+
log.Error().Err(err).Msg("Error while terminating connection")
162+
}
163+
if err := local.Close(); err != nil {
164+
log.Error().Err(err).Msg("Error while terminating connection")
165+
}
166+
}(remote, local)
167+
}
82168
}
83-
84-
<-cErr
85169
}
86170

87-
func forward(dst, src net.Conn, cErr chan<- error) {
171+
func forward(dst, src WriteCloser, cErr chan<- error) {
88172
_, err := io.Copy(dst, src)
89173
cErr <- err
90-
91-
tcpConn, ok := dst.(*net.TCPConn)
92-
if ok {
93-
if err := tcpConn.CloseWrite(); err != nil {
94-
log.Error().Err(err).Msg("Error while terminating connection")
95-
}
174+
if err := dst.CloseWrite(); err != nil {
175+
log.Error().Err(err).Msgf("error closing %s", dst.RemoteAddr().String())
96176
}
97177
}
178+
179+
type wrappedCon struct {
180+
*yamux.Stream
181+
}
182+
183+
// WrapConn wraps a stream into a wrappedCon so it implements the WriteCloser interface
184+
func WrapConn(conn *yamux.Stream) WriteCloser {
185+
return wrappedCon{conn}
186+
}
187+
188+
func (c wrappedCon) CloseWrite() error {
189+
return c.Stream.Close()
190+
}

0 commit comments

Comments
 (0)