Skip to content

Commit 4e6f6bf

Browse files
committed
Add Noise protocol encryption for rathole tunnels
Kings can now be started with --noise-private-key and --noise-public-key to enable Noise protocol (NK pattern) encryption on rathole tunnels. The public key is distributed to lings via council state, and lings automatically enable encryption when connecting to keys that have one.
1 parent 5c81790 commit 4e6f6bf

11 files changed

Lines changed: 137 additions & 4 deletions

File tree

examples/docker-swarm/stack.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ services:
2727
--host="172.17.0.1"
2828
--rathole="bind_port=2333 ports=5000-5001"
2929
--location=CPH
30+
# --noise-private-key="<base64-from-rathole-genkey>"
31+
# --noise-public-key="<base64-from-rathole-genkey>"
3032
ports:
3133
- { target: 2333, published: 2333, protocol: tcp, mode: host }
3234
- { target: 5000, published: 5000, protocol: tcp, mode: host }

readme.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@ Controlplane application starting rathole servers, must be reachable for all rat
1616
Dataplane application managing rathole clients and traefik proxies, can be completely isolated
1717

1818
### encryption
19-
Since reverse tunnel and proxy encryption isn't implemented yet, it's highly recommended that network traffic encryption is handled via other mechanisms (e.g. [Nebula](https://github.com/slackhq/nebula) or VPN), unless you are absolutely sure your traffic will stay in-house
19+
Rathole tunnel traffic between kings and lings can be encrypted using the [Noise protocol](https://noiseprotocol.org/) (NK pattern). Generate a keypair with `rathole --genkey` and pass the keys to the king:
20+
21+
```bash
22+
king --noise-private-key="<base64-private-key>" --noise-public-key="<base64-public-key>" ...
23+
```
24+
25+
The king sends its public key to the council, which distributes it to lings via state. Lings automatically enable encryption when connecting to kings that have a noise public key. Kings without noise keys continue to work unencrypted.
26+
27+
For proxy traffic or additional defense-in-depth, network-level encryption via [Nebula](https://github.com/slackhq/nebula) or VPN is still recommended.
2028

2129

2230
## Quickstart

src/cmds/king-cmd.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface KingArguments {
1414
rathole: string[];
1515
host: string;
1616
location: string;
17+
noisePrivateKey?: string;
18+
noisePublicKey?: string;
1719
}
1820

1921
export const command = "king";
@@ -61,5 +63,13 @@ export function builder (yargs: Argv) {
6163
description: "Location identifier",
6264
demandOption: true,
6365
});
66+
yargs.options("noise-private-key", {
67+
type: "string",
68+
description: "Base64 encoded Noise protocol private key (from rathole --genkey)",
69+
});
70+
yargs.options("noise-public-key", {
71+
type: "string",
72+
description: "Base64 encoded Noise protocol public key (from rathole --genkey)",
73+
});
6474
return yargs;
6575
}

src/configs/king-config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@ export class KingConfig {
1212
location: string;
1313
host: string;
1414
councilHost: string;
15+
noisePrivateKey: string | null;
16+
noisePublicKey: string | null;
1517

1618
constructor (args: KingArguments) {
1719
this.location = args.location;
1820
this.host = args.host;
1921
this.councilHost = args.councilHost;
2022

23+
const hasPrivate = args.noisePrivateKey != null;
24+
const hasPublic = args.noisePublicKey != null;
25+
assert(hasPrivate === hasPublic, "--noise-private-key and --noise-public-key must both be set or both be omitted");
26+
this.noisePrivateKey = args.noisePrivateKey ?? null;
27+
this.noisePublicKey = args.noisePublicKey ?? null;
28+
2129
for (const ratholeArg of args.rathole ?? []) {
2230
const pairs: {[key: string]: string} = {};
2331
for (const pair of ratholeArg.split(" ")) {

src/managers/king-rathole-manager.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,21 @@ export class KingRatholeManager extends ProcessManager {
3939
"[server]",
4040
`bind_addr = "0.0.0.0:${bindPort}"`,
4141
"",
42-
"[server.services]",
4342
);
4443

44+
if (this.context.config.noisePrivateKey) {
45+
lines.push(
46+
"[server.transport]",
47+
`type = "noise"`,
48+
"",
49+
"[server.transport.noise]",
50+
`local_private_key = "${this.context.config.noisePrivateKey}"`,
51+
"",
52+
);
53+
}
54+
55+
lines.push("[server.services]");
56+
4557
for (const service of services) {
4658
lines.push(
4759
`[server.services.${service.service_id.replace(/:/g, "-")}]`,

src/managers/ling-rathole-manager.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {RatholeTransform} from "../stream/rathole-transform.js";
99
export class LingRatholeManager extends ProcessManager {
1010

1111
private readonly context;
12+
private readonly noiseKeys = new Map<string, string | null>();
1213

1314
constructor (context: LingContext) {
1415
super({...context, serviceType: "ratling"});
@@ -26,15 +27,35 @@ export class LingRatholeManager extends ProcessManager {
2627
});
2728
}
2829

30+
private getNoisePublicKey (kingBindAddr: string): string | null {
31+
const [host, portStr] = kingBindAddr.split(":");
32+
const bindPort = Number(portStr);
33+
const king = this.context.state.kings.find(k => k.host === host && k.bind_port === bindPort);
34+
return king?.noise_public_key ?? null;
35+
}
36+
2937
private writeRatholeFile (kingBindAddr: string, services: StateService[], config: LingConfig, lingId: string): string {
38+
const noisePublicKey = this.getNoisePublicKey(kingBindAddr);
3039
const lines = [];
3140
lines.push(
3241
"[client]",
3342
`remote_addr = "${kingBindAddr}"`,
3443
"",
35-
"[client.services]",
3644
);
3745

46+
if (noisePublicKey) {
47+
lines.push(
48+
"[client.transport]",
49+
`type = "noise"`,
50+
"",
51+
"[client.transport.noise]",
52+
`remote_public_key = "${noisePublicKey}"`,
53+
"",
54+
);
55+
}
56+
57+
lines.push("[client.services]");
58+
3859
for (const service of services.filter(s => `${s.host}:${s.bind_port}` === kingBindAddr)) {
3960
const ratholeCnf = config.ratholeMap.get(service.name);
4061
assert(ratholeCnf != null, "ratholeCnf is undefined or null");
@@ -57,6 +78,16 @@ export class LingRatholeManager extends ProcessManager {
5778

5879
const kingBindAddrs = services.map(s => `${s.host}:${s.bind_port}`);
5980

81+
// Kill processes where the noise key has changed so they restart with updated config
82+
for (const kingBindAddr of kingBindAddrs) {
83+
const noisePublicKey = this.getNoisePublicKey(kingBindAddr);
84+
const previousKey = this.noiseKeys.get(kingBindAddr);
85+
if (previousKey !== undefined && previousKey !== noisePublicKey) {
86+
await this.killProcess(kingBindAddr, "SIGTERM");
87+
}
88+
this.noiseKeys.set(kingBindAddr, noisePublicKey);
89+
}
90+
6091
// Ensure rathole process is running and maintain rathole client configuration file
6192
for (const kingBindAddr of kingBindAddrs) {
6293
const ratholeFile = this.writeRatholeFile(kingBindAddr, services, config, lingId);

src/routes/council-route-put-king.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,21 @@ export default async function ({req, res, state, provisioner, socketIo}: RouteCt
2222
}
2323
}
2424

25+
const noisePublicKey: string | null = data["noise_public_key"] ?? null;
26+
2527
for (const rathole of data["ratholes"]) {
2628
const king = state.kings.find(k => k.ports === rathole.ports && k.host === data.host);
2729
if (king) {
30+
let changed = false;
2831
if (king.shutting_down !== data["shutting_down"]) {
2932
king.shutting_down = data["shutting_down"];
33+
changed = true;
34+
}
35+
if (king.noise_public_key !== noisePublicKey) {
36+
king.noise_public_key = noisePublicKey;
37+
changed = true;
38+
}
39+
if (changed) {
3040
state.revision++;
3141
provisioner.provision(state);
3242
socketIo.sockets.emit("state-changed");
@@ -41,6 +51,7 @@ export default async function ({req, res, state, provisioner, socketIo}: RouteCt
4151
location: data["location"],
4252
beat: Date.now(),
4353
shutting_down: false,
54+
noise_public_key: noisePublicKey,
4455
});
4556
state.revision++;
4657
provisioner.provision(state);

src/state-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface StateKing {
1111
shutting_down: boolean;
1212
beat: number;
1313
location: string;
14+
noise_public_key: string | null;
1415
}
1516

1617
export interface StateLing {

src/tickers/king-syncer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export class KingSyncer extends Ticker {
2424
ratholes: this.context.config.ratholes,
2525
ready_service_ids: this.context.readyServiceIds,
2626
location: this.context.config.location,
27+
noise_public_key: this.context.config.noisePublicKey,
2728
},
2829
}));
2930
if (err || response.statusCode !== 200) {

tests/council-provisioner.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ test("Find available port on king", () => {
2727
remote_port: null,
2828
},
2929
],
30-
kings: [{host: "kinghost.com", ports: "5000-5000", location: "myhouse", bind_port: 2343, beat: 0, shutting_down: false}],
30+
kings: [{host: "kinghost.com", ports: "5000-5000", location: "myhouse", bind_port: 2343, beat: 0, shutting_down: false, noise_public_key: null}],
3131
lings: [{ling_id: "some_ling_id", beat: 0, shutting_down: false}],
3232
revision: 0,
3333
};
@@ -43,6 +43,7 @@ test("Find available port on king", () => {
4343
"location": "myhouse",
4444
"ports": "5000-5000",
4545
"shutting_down": false,
46+
"noise_public_key": null,
4647
},
4748
"ports": [5000],
4849
},

0 commit comments

Comments
 (0)