Skip to content

Commit

Permalink
Use random port for mux/demux server, fixes #7
Browse files Browse the repository at this point in the history
  • Loading branch information
boronine committed Nov 4, 2024
1 parent 34f96e5 commit 8b016e2
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 104 deletions.
76 changes: 53 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,57 +30,79 @@ the server, and both are configured to reject anything else. This way, the pair

## Usage


```
usage: h2tunnel <command> [options]
commands:
client
server
client options:
--crt <path> Path to certificate file (.crt)
--key <path> Path to private key file (.key)
--tunnel-host <host> Host for the tunnel server
--tunnel-port <port> Port for the tunnel server
--origin-host <port> Host for the local TCP server (default: localhost)
--origin-port <port> Port for the local TCP server
server options:
--crt <path> Path to certificate file (.crt)
--key <path> Path to private key file (.key)
--tunnel-listen-ip <ip> IP for the tunnel server to bind on (default: 0.0.0.0)
--tunnel-listen-port <port> Port for the tunnel server to listen on
--proxy-listen-ip <port> IP for the remote TCP proxy server to bind on (default: 0.0.0.0)
--proxy-listen-port <port> Port for the remote TCP proxy server to listen on
The tunnel and proxy servers will bind to 0.0.0.0 by default which will make them publically available. This requires
superuser permissions on Linux. You can change this setting to bind to a specific network interface, e.g. a VPN, but
this is advanced usage.
```

Generate `h2tunnel.key` and `h2tunnel.crt` files using `openssl` command:

```bash
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:secp384r1 -days 3650 -nodes -keyout h2tunnel.key -out h2tunnel.crt -subj "/CN=localhost"
```

### Forward localhost:8000 to http://example.com
### Forward localhost:8000 to http://mysite.example.com

On your server (example.com), we will be listening for tunnel connections on port 15001, and providing an HTTP proxy
on port 80. Make sure these are open in your firewall.

Use any port for `--mux-listen-port`, h2tunnel will run an HTTP2 multiplexer on this port bound to 127.0.0.1,
it will not be exposed to the internet even if your firewall allows it.

```bash
# sudo is required to bind to 0.0.0.0, which makes the tunnel server and proxy server publically available
sudo h2tunnel server \
--crt h2tunnel.crt \
--key h2tunnel.key \
--tunnel-listen-ip 0.0.0.0 \
--tunnel-listen-port 15001 \
--proxy-listen-ip 0.0.0.0 \
--proxy-listen-port 80 \
--mux-listen-port=15002
--proxy-listen-port 80
````

On your local machine, we will connect to the tunnel and forward a local HTTP server on port 8000.

Use any port for `--demux-listen-port`, h2tunnel will run an HTTP2 demultiplexer on it.

```bash
python3 -m http.server # runs on port 8000
python3 -m http.server # runs on port 8000 by default
h2tunnel client \
--crt h2tunnel.crt \
--key h2tunnel.key \
--tunnel-host=example.com \
--tunnel-host=mysite.example.com \
--tunnel-port=15001 \
--local-http-port=8000 \
--demux-listen-port=15004
--local-http-port=8000
```

### Forward localhost:8000 to https://example.com
### Forward localhost:8000 to https://mysite.example.com

This is the same as the previous example, but with an extra layer: a [Caddy](https://caddyserver.com/) reverse proxy
that will auto-provision TLS certificates for your domain. This is useful if you want to expose an HTTPS server.
that will auto-provision TLS certificates for your domain. This is useful if you want to expose a local HTTP server
as HTTPS.

The client command line is the same as before, but for the server we will use a docker compose setup.

Specify your domain in the `.env` file:

```
TUNNEL_DOMAIN=example.com
TUNNEL_DOMAIN=mysite.example.com
```

Push the necessary files to the server:
Expand All @@ -106,15 +128,18 @@ const client = new TunnelClient({
logger: (line) => console.log(line), // optional
key: `-----BEGIN PRIVATE KEY----- ...`,
cert: `-----BEGIN CERTIFICATE----- ...`,
demuxListenPort: 15004,
localHttpPort: 8000,
originHost: "localhost", // optional
originPort: 8000,
tunnelHost: `mysite.example.com`,
tunnelPort: 15001,
});
// Start the client
client.start();
// Wait until client is connected
await client.waitUntilConnected();
// Stop the client
await client.stop();
```
Expand All @@ -124,18 +149,23 @@ import {TunnelServer} from "h2tunnel";
const server = new TunnelServer({
logger: (line) => console.log(line), // optional
tunnelListenIp: "0.0.0.0",
tunnelListenPort: 15001,
key: `-----BEGIN PRIVATE KEY----- ...`,
cert: `-----BEGIN CERTIFICATE----- ...`,
tunnelListenIp: "0.0.0.0", // optional
tunnelListenPort: 15001,
proxyListenIp: "0.0.0.0", // optional
proxyListenPort: 80,
proxyListenIp: "0.0.0.0",
muxListenPort: 15002,
});
// Start the server
server.start();
// Wait until server is listening
await client.waitUntilListening();
// Wait until server is connected
await client.waitUntilConnected();
// Stop the server
await server.stop();
```
Expand Down
45 changes: 25 additions & 20 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#!/usr/bin/env node
import { parseArgs } from "node:util";
import { AbstractTunnel, TunnelClient, TunnelServer } from "./h2tunnel.js";
import {
AbstractTunnel,
DEFAULT_LISTEN_IP,
DEFAULT_ORIGIN_HOST,
TunnelClient,
TunnelServer,
} from "./h2tunnel.js";
import * as fs from "node:fs";

const { positionals, values } = parseArgs({
Expand All @@ -24,20 +30,17 @@ const { positionals, values } = parseArgs({
"proxy-listen-port": {
type: "string",
},
"mux-listen-port": {
type: "string",
},
// Client
"tunnel-host": {
type: "string",
},
"tunnel-port": {
type: "string",
},
"local-http-port": {
"origin-host": {
type: "string",
},
"demux-listen-port": {
"origin-port": {
type: "string",
},
},
Expand Down Expand Up @@ -79,17 +82,20 @@ client options:
--${"key" satisfies Param} <path> Path to private key file (.key)
--${"tunnel-host" satisfies Param} <host> Host for the tunnel server
--${"tunnel-port" satisfies Param} <port> Port for the tunnel server
--${"local-http-port" satisfies Param} <port> Port for the local HTTP server
--${"demux-listen-port" satisfies Param} <port> Port for the HTTP2 server to listen on
--${"origin-host" satisfies Param} <port> Host for the local TCP server (default: ${DEFAULT_ORIGIN_HOST})
--${"origin-port" satisfies Param} <port> Port for the local TCP server
server options:
--${"crt" satisfies Param} <path> Path to certificate file (.crt)
--${"key" satisfies Param} <path> Path to private key file (.key)
--${"tunnel-listen-ip" satisfies Param} <ip> IP for the tunnel server to bind on (use 0.0.0.0 for all interfaces)
--${"tunnel-listen-ip" satisfies Param} <ip> IP for the tunnel server to bind on (default: ${DEFAULT_LISTEN_IP})
--${"tunnel-listen-port" satisfies Param} <port> Port for the tunnel server to listen on
--${"proxy-listen-ip" satisfies Param} <port> Host for the remote HTTP server (use 0.0.0.0 for all interfaces)
--${"proxy-listen-port" satisfies Param} <port> Port for the remote HTTP server
--${"mux-listen-port" satisfies Param} <port> Port for the HTTP2 server to listen on
--${"proxy-listen-ip" satisfies Param} <port> IP for the remote TCP proxy server to bind on (default: ${DEFAULT_LISTEN_IP})
--${"proxy-listen-port" satisfies Param} <port> Port for the remote TCP proxy server to listen on
The tunnel and proxy servers will bind to 0.0.0.0 by default which will make them publically available. This requires
superuser permissions on Linux. You can change this setting to bind to a specific network interface, e.g. a VPN, but
this is advanced usage.
`;

if (positionals.length === 0) {
Expand All @@ -99,22 +105,21 @@ if (positionals.length === 0) {
let tunnel: AbstractTunnel<any, any>;
if (command === "client") {
tunnel = new TunnelClient({
tunnelHost: getString("tunnel-host"),
tunnelPort: getInt("tunnel-port"),
key: fs.readFileSync(getString("key"), "utf8"),
cert: fs.readFileSync(getString("crt"), "utf8"),
localHttpPort: getInt("local-http-port"),
demuxListenPort: getInt("demux-listen-port"),
tunnelHost: getString("tunnel-host"),
tunnelPort: getInt("tunnel-port"),
originHost: values["origin-host" satisfies Param],
originPort: getInt("origin-port"),
});
} else if (command === "server") {
tunnel = new TunnelServer({
tunnelListenIp: getString("tunnel-listen-ip"),
tunnelListenPort: getInt("tunnel-listen-port"),
key: fs.readFileSync(getString("key"), "utf8"),
cert: fs.readFileSync(getString("crt"), "utf8"),
tunnelListenIp: values["tunnel-listen-ip" satisfies Param],
tunnelListenPort: getInt("tunnel-listen-port"),
proxyListenIp: values["proxy-listen-ip" satisfies Param],
proxyListenPort: getInt("proxy-listen-port"),
proxyListenIp: getString("proxy-listen-ip"),
muxListenPort: getInt("mux-listen-port"),
});
} else {
throw new Error(`Unknown command: ${command}`);
Expand Down
22 changes: 8 additions & 14 deletions src/h2tunnel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,16 @@ import {
TunnelServer,
} from "./h2tunnel.js";
import net from "node:net";
import * as http2 from "node:http2";

// localhost HTTP1 server "python3 -m http.server"
// localhost echo server
const LOCAL_PORT = 14000;
// localhost HTTP2 server that proxies to localhost HTTP1 server
const DEMUX_PORT = 14003;

// remote public HTTP1 server
const PROXY_PORT = 14004;
const PROXY_TEST_PORT = 14007;
// remote TLS server for establishing a tunnel
const TUNNEL_PORT = 14005;
const TUNNEL_TEST_PORT = 14008;
// remote HTTPS server that is piped through the tunnel to localhost
const MUX_PORT = 14006;

// Reduce this to make tests faster
const TIME_MULTIPLIER = 0.1;
Expand Down Expand Up @@ -53,23 +48,22 @@ const getLogger = (name: string, colorCode: number) => (line: object) =>

const serverOptions: ServerOptions = {
logger: getLogger("server", 32),
tunnelListenIp: "127.0.0.1",
tunnelListenPort: TUNNEL_PORT,
key: CLIENT_KEY,
cert: CLIENT_CRT,
proxyListenPort: PROXY_PORT,
tunnelListenIp: "127.0.0.1",
tunnelListenPort: TUNNEL_PORT,
proxyListenIp: "127.0.0.1",
muxListenPort: MUX_PORT,
proxyListenPort: PROXY_PORT,
};

const clientOptions: ClientOptions = {
logger: getLogger("client", 33),
tunnelHost: "localhost",
tunnelPort: TUNNEL_PORT,
key: CLIENT_KEY,
cert: CLIENT_CRT,
localHttpPort: LOCAL_PORT,
demuxListenPort: DEMUX_PORT,
tunnelHost: "localhost",
tunnelPort: TUNNEL_PORT,
originHost: "localhost",
originPort: LOCAL_PORT,
tunnelRestartTimeout: 500 * TIME_MULTIPLIER,
};

Expand Down
Loading

0 comments on commit 8b016e2

Please sign in to comment.