Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/docker-swarm/stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ services:
--host="172.17.0.1"
--rathole="bind_port=2333 ports=5000-5001"
--location=CPH
# --noise-private-key="<base64-from-rathole-genkey>"
# --noise-public-key="<base64-from-rathole-genkey>"
ports:
- { target: 2333, published: 2333, protocol: tcp, mode: host }
- { target: 5000, published: 5000, protocol: tcp, mode: host }
Expand Down
10 changes: 9 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ Controlplane application starting rathole servers, must be reachable for all rat
Dataplane application managing rathole clients and traefik proxies, can be completely isolated

### encryption
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
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:

```bash
king --noise-private-key="<base64-private-key>" --noise-public-key="<base64-public-key>" ...
```

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.

For proxy traffic or additional defense-in-depth, network-level encryption via [Nebula](https://github.com/slackhq/nebula) or VPN is still recommended.


## Quickstart
Expand Down
10 changes: 10 additions & 0 deletions src/cmds/king-cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface KingArguments {
rathole: string[];
host: string;
location: string;
noisePrivateKey?: string;
noisePublicKey?: string;
}

export const command = "king";
Expand Down Expand Up @@ -61,5 +63,13 @@ export function builder (yargs: Argv) {
description: "Location identifier",
demandOption: true,
});
yargs.options("noise-private-key", {
type: "string",
description: "Base64 encoded Noise protocol private key (from rathole --genkey)",
});
yargs.options("noise-public-key", {
type: "string",
description: "Base64 encoded Noise protocol public key (from rathole --genkey)",
});
return yargs;
}
8 changes: 8 additions & 0 deletions src/configs/king-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,20 @@ export class KingConfig {
location: string;
host: string;
councilHost: string;
noisePrivateKey: string | null;
noisePublicKey: string | null;

constructor (args: KingArguments) {
this.location = args.location;
this.host = args.host;
this.councilHost = args.councilHost;

const hasPrivate = args.noisePrivateKey != null;
const hasPublic = args.noisePublicKey != null;
assert(hasPrivate === hasPublic, "--noise-private-key and --noise-public-key must both be set or both be omitted");
this.noisePrivateKey = args.noisePrivateKey ?? null;
this.noisePublicKey = args.noisePublicKey ?? null;

for (const ratholeArg of args.rathole ?? []) {
const pairs: {[key: string]: string} = {};
for (const pair of ratholeArg.split(" ")) {
Expand Down
14 changes: 13 additions & 1 deletion src/managers/king-rathole-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,21 @@ export class KingRatholeManager extends ProcessManager {
"[server]",
`bind_addr = "0.0.0.0:${bindPort}"`,
"",
"[server.services]",
);

if (this.context.config.noisePrivateKey) {
lines.push(
"[server.transport]",
"type = \"noise\"",
"",
"[server.transport.noise]",
`local_private_key = "${this.context.config.noisePrivateKey}"`,
"",
);
}

lines.push("[server.services]");

for (const service of services) {
lines.push(
`[server.services.${service.service_id.replace(/:/g, "-")}]`,
Expand Down
33 changes: 32 additions & 1 deletion src/managers/ling-rathole-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {RatholeTransform} from "../stream/rathole-transform.js";
export class LingRatholeManager extends ProcessManager {

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

constructor (context: LingContext) {
super({...context, serviceType: "ratling"});
Expand All @@ -26,15 +27,35 @@ export class LingRatholeManager extends ProcessManager {
});
}

private getNoisePublicKey (kingBindAddr: string): string | null {
const [host, portStr] = kingBindAddr.split(":");
const bindPort = Number(portStr);
const king = this.context.state.kings.find(k => k.host === host && k.bind_port === bindPort);
return king?.noise_public_key ?? null;
}

private writeRatholeFile (kingBindAddr: string, services: StateService[], config: LingConfig, lingId: string): string {
const noisePublicKey = this.getNoisePublicKey(kingBindAddr);
const lines = [];
lines.push(
"[client]",
`remote_addr = "${kingBindAddr}"`,
"",
"[client.services]",
);

if (noisePublicKey) {
lines.push(
"[client.transport]",
"type = \"noise\"",
"",
"[client.transport.noise]",
`remote_public_key = "${noisePublicKey}"`,
"",
);
}

lines.push("[client.services]");

for (const service of services.filter(s => `${s.host}:${s.bind_port}` === kingBindAddr)) {
const ratholeCnf = config.ratholeMap.get(service.name);
assert(ratholeCnf != null, "ratholeCnf is undefined or null");
Expand All @@ -57,6 +78,16 @@ export class LingRatholeManager extends ProcessManager {

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

// Kill processes where the noise key has changed so they restart with updated config
for (const kingBindAddr of kingBindAddrs) {
const noisePublicKey = this.getNoisePublicKey(kingBindAddr);
const previousKey = this.noiseKeys.get(kingBindAddr);
if (previousKey !== undefined && previousKey !== noisePublicKey) {
await this.killProcess(kingBindAddr, "SIGTERM");
}
this.noiseKeys.set(kingBindAddr, noisePublicKey);
}

// Ensure rathole process is running and maintain rathole client configuration file
for (const kingBindAddr of kingBindAddrs) {
const ratholeFile = this.writeRatholeFile(kingBindAddr, services, config, lingId);
Expand Down
11 changes: 11 additions & 0 deletions src/routes/council-route-put-king.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,21 @@ export default async function ({req, res, state, provisioner, socketIo}: RouteCt
}
}

const noisePublicKey: string | null = data["noise_public_key"] ?? null;

for (const rathole of data["ratholes"]) {
const king = state.kings.find(k => k.ports === rathole.ports && k.host === data.host);
if (king) {
let changed = false;
if (king.shutting_down !== data["shutting_down"]) {
king.shutting_down = data["shutting_down"];
changed = true;
}
if (king.noise_public_key !== noisePublicKey) {
king.noise_public_key = noisePublicKey;
changed = true;
}
if (changed) {
state.revision++;
provisioner.provision(state);
socketIo.sockets.emit("state-changed");
Expand All @@ -41,6 +51,7 @@ export default async function ({req, res, state, provisioner, socketIo}: RouteCt
location: data["location"],
beat: Date.now(),
shutting_down: false,
noise_public_key: noisePublicKey,
});
state.revision++;
provisioner.provision(state);
Expand Down
1 change: 1 addition & 0 deletions src/state-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface StateKing {
shutting_down: boolean;
beat: number;
location: string;
noise_public_key: string | null;
}

export interface StateLing {
Expand Down
1 change: 1 addition & 0 deletions src/tickers/king-syncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class KingSyncer extends Ticker {
ratholes: this.context.config.ratholes,
ready_service_ids: this.context.readyServiceIds,
location: this.context.config.location,
noise_public_key: this.context.config.noisePublicKey,
},
}));
if (err || response.statusCode !== 200) {
Expand Down
3 changes: 2 additions & 1 deletion tests/council-provisioner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ test("Find available port on king", () => {
remote_port: null,
},
],
kings: [{host: "kinghost.com", ports: "5000-5000", location: "myhouse", bind_port: 2343, beat: 0, shutting_down: false}],
kings: [{host: "kinghost.com", ports: "5000-5000", location: "myhouse", bind_port: 2343, beat: 0, shutting_down: false, noise_public_key: null}],
lings: [{ling_id: "some_ling_id", beat: 0, shutting_down: false}],
revision: 0,
};
Expand All @@ -43,6 +43,7 @@ test("Find available port on king", () => {
"location": "myhouse",
"ports": "5000-5000",
"shutting_down": false,
"noise_public_key": null,
},
"ports": [5000],
},
Expand Down
48 changes: 48 additions & 0 deletions tests/council-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,53 @@ describe("PUT /king", () => {
expect(res.text).toEqual("ok");
expect(res.statusCode).toEqual(200);
});

test("stores noise_public_key on king creation", async () => {
const server = createServer({provisioner, state}).httpServer;
await request(server).put("/king").send({
ratholes: [{bind_port: 2333, ports: "5000-5001"}],
ready_service_ids: [],
location: "mylocation",
host: "example.com",
noise_public_key: "abc123pubkey",
});
expect(state.kings).toHaveLength(1);
expect(state.kings[0].noise_public_key).toEqual("abc123pubkey");
});

test("stores null noise_public_key when not provided", async () => {
const server = createServer({provisioner, state}).httpServer;
await request(server).put("/king").send({
ratholes: [{bind_port: 2333, ports: "5000-5001"}],
ready_service_ids: [],
location: "mylocation",
host: "example.com",
});
expect(state.kings).toHaveLength(1);
expect(state.kings[0].noise_public_key).toBeNull();
});

test("updates noise_public_key on existing king", async () => {
state.kings.push({
bind_port: 2333,
ports: "5000-5001",
host: "example.com",
location: "mylocation",
beat: 0,
shutting_down: false,
noise_public_key: null,
});
const initialRevision = state.revision;
const server = createServer({provisioner, state}).httpServer;
await request(server).put("/king").send({
ratholes: [{bind_port: 2333, ports: "5000-5001"}],
ready_service_ids: [],
location: "mylocation",
host: "example.com",
noise_public_key: "newkey123",
});
expect(state.kings[0].noise_public_key).toEqual("newkey123");
expect(state.revision).toBeGreaterThan(initialRevision);
});
});

Loading