tcpwarp/
lib.rs

1/*!
2# A userspace tunnel between two hosts mapping ports on client machine to addresses reachable from server machine
3
4```bash
5tcp-warp server
6tcp-warp client -c 8080:towel.blinkenlights.nl:23
7nc 127.0.0.1 8080
8```
9
10## Features
11
121. A userspace tunnel to connect ports on client network with connections available on server side.
131. Uses only single port.
141. Client push of addresses to connect from server.
15
16## Installation
17
18With [cargo](https://www.rust-lang.org/learn/get-started):
19
20```bash
21cargo install tcp-warp-cli
22```
23
24## Usage
25
26To create a tunnel we need to start a server listening on some port and then connect to it with a client.
27
28### Docker usage for server part
29
30```bash
31docker run --rm -d -p 18000:18000 tcpwarp/tcpwarp
32```
33
34or with custom listen port (ex: 18234):
35
36```bash
37docker run --rm -d -p 18234:18234 tcpwarp/tcpwarp tcp-warp server --listen=0.0.0.0:18234
38```
39
40### Simple local running port remapper
41
421. Start server:
43
44    ```bash
45    tcp-warp server
46    ```
47
481. Start client:
49
50    ```bash
51    tcp-warp client -c 8080:towel.blinkenlights.nl:23
52    ```
53
541. Enjoy the show:
55
56    ```bash
57    nc 127.0.0.1 8080
58    ```
59
601. This example uses default listen and connect interfaces. In a real life scenario you need at least provide -t / --tunnel parameter to client:
61
62    ```bash
63    tcp-warp client -t host:port ...
64    ```
65
66Both client and server have address on which they listen for incoming connections and client additionally have parameter to specify connection address.
67
68Next we look at more specific example.
69
70### Use case: running Docker on machine without Docker daemon installed with Docker daemon behind SSH
71
72Background:
73
74- client: client machine runs on Windows, has Windows version of `tcp-warp` and Docker CLI installed. Client cannot run Docker daemon.
75- public: master node accessible with SSH from which Docker daemon node can be accessed.
76- docker: docker daemon node accessible with SSH.
77
78Target:
79
80Run Docker over tcp transport, allowing `client` to build and run containers. Environment should be available for each developer independent of other.
81
82Solution:
83
84Run on `docker` machine Docker-in-Docker container (`dind`) using tcp host protocol. Use `DOCKER_HOST` environment variable on `client` to connect to `dind`. `dind` is bindet to host port on `docker` host and forwarded via `public` with SSH port-forwarding.
85
86The sequence of commands can be following:
87
88#### Initial sequence (installation)
89
901. Go to `docker` node and start required containers:
91
92    ```bash
93    user@client $ ssh user1@public
94    user1@public $ ssh user2@docker
95    user2@docker $ docker run --rm --privileged -p 2375:2375 -p 18000:18000 -d --name some-docker docker:dind dockerd --host=tcp://0.0.0.0:2375
96    user2@docker $ DOCKER_HOST=tcp://127.0.0.1:2375 docker run --rm -p 18000:18000 -d --name some-docker-tcp-warp tcpwarp/tcpwarp
97    ```
98
991. Disconnect from `docker` and `public` nodes.
100
101#### Normal sequence (usage)
102
1031. Connect to `public` node with `ssh` and forward port for `tcp-warp`:
104
105    ```bash
106    ssh -L 18000:docker:18000 user1@public
107    ```
108
1091. Connect to Docker daemon with `tcp-warp client` on `client` machine:
110
111    ```bash
112    tcp-warp client -c 10001:172.18.0.1:2375
113    ```
114
115    `172.18.0.1` here is the address of host node in `dind`.
116
1171. Export DOCKER_HOST environment variable on `client` machine:
118
119    ```bash
120    export DOCKER_HOST=tcp://127.0.0.1:10001
121    ```
122
1231. Run docker commands from `client`:
124
125    ```bash
126    docker ps
127    docker run hello-world
128    docker run -it alpine ash
129    ```
130
131#### Additional services
132
133We can start additional services and relaunch `tcp-warp client` with additional `-c` for these services.
134
135Simple example with `whoami` service:
136
1371. Create network to use for hostname resolution. Start `whoami` service with all above steps done. Connect tcp-warp container to new network:
138
139    ```bash
140    docker network create our-network
141    docker run --rm -d --net our-network --name whoami containous/whoami
142    docker network connect our-network some-docker-tcp-warp
143    ```
144
1451. Stop `tcp-warp client`. Start it with additional port mapping for `whoami` service:
146
147    ```bash
148    tcp-warp client -c 10001:172.18.0.1:2375 -c 8080:whoami:80
149    ```
150
1511. Test `whoami` service:
152
153    ```bash
154    $ curl http://localhost:8080/
155    Hostname: 9fe704cf0e87
156    IP: 127.0.0.1
157    IP: 172.18.0.3
158    IP: 172.19.0.3
159    RemoteAddr: 172.19.0.2:44612
160    GET / HTTP/1.1
161    Host: localhost:8080
162    User-Agent: curl/7.64.1
163    ```
164
165*/
166use bytes::{Buf, BufMut, BytesMut};
167use futures::{
168    future::{abortable, AbortHandle},
169    prelude::*,
170    try_join,
171};
172use log::*;
173use std::{
174    collections::HashMap,
175    convert::TryInto,
176    error::Error,
177    net::{IpAddr, SocketAddr},
178    str::FromStr,
179    sync::Arc,
180    time::Duration,
181};
182use tokio::{
183    net::{TcpListener, TcpStream, ToSocketAddrs},
184    prelude::*,
185    spawn,
186    sync::{
187        mpsc::{channel, Sender},
188        oneshot,
189    },
190    time::delay_for,
191};
192use tokio_util::codec::{Decoder, Encoder, Framed};
193use uuid::Uuid;
194
195mod client;
196mod proto;
197mod server;
198
199pub use client::TcpWarpClient;
200pub use proto::{TcpWarpMessage, TcpWarpProto, TcpWarpProtoClient, TcpWarpProtoHost};
201pub use server::TcpWarpServer;
202
203#[derive(Debug, Clone, PartialEq)]
204pub struct TcpWarpPortConnection {
205    client_port: Option<u16>,
206    host: Option<String>,
207    port: u16,
208}
209
210impl FromStr for TcpWarpPortConnection {
211    type Err = io::Error;
212
213    fn from_str(s: &str) -> Result<Self, Self::Err> {
214        let mut parts = s.split(':');
215
216        match (parts.next(), parts.next(), parts.next(), parts.next()) {
217            (Some(client_port), host, Some(port), None) => {
218                match (client_port.parse(), port.parse()) {
219                    (Ok(client_port), Ok(port)) => Ok(TcpWarpPortConnection {
220                        client_port: Some(client_port),
221                        host: host.map(str::to_owned),
222                        port,
223                    }),
224                    _ => Err(io::Error::new(
225                        io::ErrorKind::Other,
226                        "cannot parse port mapping",
227                    )),
228                }
229            }
230            (Some(client_port_or_host), Some(port), None, None) => {
231                let port = match port.parse() {
232                    Ok(port) => port,
233                    Err(_) => {
234                        return Err(io::Error::new(
235                            io::ErrorKind::Other,
236                            "cannot parse port mapping",
237                        ))
238                    }
239                };
240                let client_port: Result<u16, _> = client_port_or_host.parse();
241                if let Ok(client_port) = client_port {
242                    Ok(TcpWarpPortConnection {
243                        client_port: Some(client_port),
244                        host: None,
245                        port,
246                    })
247                } else {
248                    Ok(TcpWarpPortConnection {
249                        client_port: None,
250                        host: Some(client_port_or_host.to_owned()),
251                        port,
252                    })
253                }
254            }
255            (Some(port), None, None, None) => {
256                let port = match port.parse() {
257                    Ok(port) => port,
258                    Err(_) => {
259                        return Err(io::Error::new(
260                            io::ErrorKind::Other,
261                            "cannot parse port mapping",
262                        ))
263                    }
264                };
265                Ok(TcpWarpPortConnection {
266                    client_port: None,
267                    host: None,
268                    port,
269                })
270            }
271            _ => Err(io::Error::new(
272                io::ErrorKind::Other,
273                "cannot parse port mapping",
274            )),
275        }
276    }
277}
278
279pub struct TcpWarpConnection {
280    sender: Sender<TcpWarpMessage>,
281    connected_sender: Option<oneshot::Sender<Result<(), io::Error>>>,
282}
283
284#[cfg(test)]
285mod tests {
286
287    use super::*;
288
289    #[test]
290    fn connection_from_str() {
291        assert_eq!(
292            Ok(TcpWarpPortConnection {
293                client_port: None,
294                host: None,
295                port: 8080
296            }),
297            "8080".parse().map_err(|_| ())
298        );
299        assert_eq!(
300            Ok(TcpWarpPortConnection {
301                client_port: Some(8081),
302                host: None,
303                port: 8080
304            }),
305            "8081:8080".parse().map_err(|_| ())
306        );
307        assert_eq!(
308            Ok(TcpWarpPortConnection {
309                client_port: Some(8081),
310                host: Some("localhost".into()),
311                port: 8080
312            }),
313            "8081:localhost:8080".parse().map_err(|_| ())
314        );
315    }
316}