Skip to content
Merged
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
13 changes: 11 additions & 2 deletions packages/admin-ui/src/__mock-backend__/wireguard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ const devicesState = new Set<string>(initialDevices);

export const wireguard: Pick<
Routes,
"wireguardDeviceAdd" | "wireguardDeviceGet" | "wireguardDeviceRemove" | "wireguardDevicesGet"
| "wireguardDeviceAdd"
| "wireguardDeviceConfigGet"
| "wireguardDeviceGet"
| "wireguardDeviceRemove"
| "wireguardDevicesGet"
> = {
wireguardDeviceAdd: async (id) => {
devicesState.add(id);
Expand All @@ -14,7 +18,7 @@ export const wireguard: Pick<
devicesState.delete(id);
},
wireguardDevicesGet: async () => Array.from(devicesState.values()),
wireguardDeviceGet: async (id) => {
wireguardDeviceConfigGet: async ({ device: id, isLocal }) => {
if (!devicesState.has(id)) throw Error(`No device id ${id}`);
const configRemote = `[Interface]
Address = 172.34.1.2
Expand All @@ -37,6 +41,11 @@ PublicKey = AAAAABBBBBAAAAABBBBBAAAAABBBBBAAAAABBBBBAAA=
Endpoint = 192.168.1.45:51820
AllowedIPs = 172.33.0.0/16`;

return isLocal ? configLocal : configRemote;
},
wireguardDeviceGet: async (id) => {
const configRemote = await wireguard.wireguardDeviceConfigGet({ device: id, isLocal: false });
const configLocal = await wireguard.wireguardDeviceConfigGet({ device: id, isLocal: true });
return { configRemote, configLocal };
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,28 @@ import { apiRoutes } from "api";
import { FiDownload } from "react-icons/fi";
import { GoCopy } from "react-icons/go";
import { FaQrcode } from "react-icons/fa";
import { WireguardDeviceCredentials } from "@dappnode/types";
// Utils
import { urlJoin } from "utils/url";
import SubTitle from "components/SubTitle";

function WireguardDeviceDetailsLoaded({ id, device }: { id: string; device: WireguardDeviceCredentials }) {
function WireguardDeviceDetailsLoaded({
id,
config,
showLocalCreds,
setShowLocalCreds,
isLoadingLocalConfig,
localConfigError
}: {
id: string;
config: string;
showLocalCreds: boolean;
setShowLocalCreds: React.Dispatch<React.SetStateAction<boolean>>;
isLoadingLocalConfig?: boolean;
localConfigError?: Error;
}) {
const [showQr, setShowQr] = useState(false);
const [showLocalCreds, setShowLocalCreds] = useState(false);
const config = showLocalCreds ? device.configLocal : device.configRemote;
const configType = showLocalCreds ? "local" : "";
const isShowingLocalConfig = showLocalCreds && !isLoadingLocalConfig && !localConfigError;
const configType = isShowingLocalConfig ? "local" : "";

useEffect(() => {
// Activate the copy functionality
Expand Down Expand Up @@ -55,7 +67,7 @@ function WireguardDeviceDetailsLoaded({ id, device }: { id: string; device: Wire
<a
href={apiRoutes.downloadWireguardConfig({
device: id,
isLocal: showLocalCreds
isLocal: isShowingLocalConfig
})}
>
<Button>
Expand Down Expand Up @@ -83,6 +95,18 @@ function WireguardDeviceDetailsLoaded({ id, device }: { id: string; device: Wire
</Button>
</div>

{isLoadingLocalConfig && (
<div className="alert alert-secondary" role="alert">
Loading local credentials. Remote credentials are shown below.
</div>
)}

{localConfigError && (
<div className="alert alert-warning" role="alert">
Local credentials could not be loaded. Use the remote credentials shown below instead.
</div>
)}

<Form.Group>
<Form.Label>VPN {configType} credentials</Form.Label>
<div className="credentials-config">{config}</div>
Expand All @@ -97,20 +121,53 @@ function WireguardDeviceDetailsLoaded({ id, device }: { id: string; device: Wire
);
}

function WireguardDeviceDetailsWithLocal({
id,
configRemote,
setShowLocalCreds
}: {
id: string;
configRemote: string;
setShowLocalCreds: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const localConfig = useApi.wireguardDeviceConfigGet({ device: id, isLocal: true });

return (
<WireguardDeviceDetailsLoaded
id={id}
config={localConfig.data || configRemote}
showLocalCreds={true}
setShowLocalCreds={setShowLocalCreds}
isLoadingLocalConfig={localConfig.isValidating && !localConfig.data && !localConfig.error}
localConfigError={localConfig.error}
/>
);
}

export const WireguardDeviceDetails: React.FC = () => {
const params = useParams();
const id = params.id || "";
const device = useApi.wireguardDeviceGet(id);
const [showLocalCreds, setShowLocalCreds] = useState(false);
const config = useApi.wireguardDeviceConfigGet({ device: id, isLocal: false });

return (
<>
<SubTitle>{id}</SubTitle>

{device.data ? (
<WireguardDeviceDetailsLoaded id={id} device={device.data} />
) : device.error ? (
<ErrorView error={device.error} />
) : device.isValidating ? (
{config.data ? (
showLocalCreds ? (
<WireguardDeviceDetailsWithLocal id={id} configRemote={config.data} setShowLocalCreds={setShowLocalCreds} />
) : (
<WireguardDeviceDetailsLoaded
id={id}
config={config.data}
showLocalCreds={showLocalCreds}
setShowLocalCreds={setShowLocalCreds}
/>
)
) : config.error ? (
<ErrorView error={config.error} />
) : config.isValidating ? (
<Loading steps={["Loading device credentials"]} />
) : null}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ interface Params {
* - /device_admin/
*/
export const downloadWireguardConfig = wrapHandlerHtml<Params>(async (req, res) => {
const isLocal = req.query.local === "" || req.query.local;
const isLocal = req.query.local === "" || Boolean(req.query.local);

const device = req.params.device;
if (!device) throw Error(`Must provide device`);

const { configRemote, configLocal } = await calls.wireguardDeviceGet(device);
const config = await calls.wireguardDeviceConfigGet({ device, isLocal });

const filename = `wireguard-${isLocal ? "local" : ""}config-${device}.txt`;
const mimetype = "text/plain";
res.setHeader("Content-disposition", "attachment; filename=" + filename);
res.setHeader("Content-type", mimetype);

res.status(200).send(isLocal ? configLocal : configRemote);
res.status(200).send(config);
});
43 changes: 35 additions & 8 deletions packages/dappmanager/src/calls/wireguard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { params } from "@dappnode/params";
import { packageSetEnvironment } from "./packageSetEnvironment.js";
import { ComposeFileEditor } from "@dappnode/dockercompose";
import { WireguardDeviceCredentials } from "@dappnode/types";
import { logs } from "@dappnode/logger";

const { WIREGUARD_API_URL, WIREGUARD_DEVICES_ENVNAME, WIREGUARD_DNP_NAME, WIREGUARD_ISCORE, WIREGUARD_MAIN_SERVICE } =
params;
Expand All @@ -16,7 +17,10 @@ class WireguardClient {
const service = compose.services()[WIREGUARD_MAIN_SERVICE];
if (!service) throw Error(`Wireguard service ${WIREGUARD_MAIN_SERVICE} does not exist`);
const peersCsv = service.getEnvs()[WIREGUARD_DEVICES_ENVNAME] || "";
return peersCsv.split(",");
return peersCsv
.split(",")
.map((device) => device.trim())
.filter(Boolean);
}

async addDevice(device: string): Promise<void> {
Expand Down Expand Up @@ -53,16 +57,35 @@ class WireguardClient {
// - local: '/dappnode_admin?local'
// - local qr: '/dappnode_admin?local&qr'
async getDeviceCredentials(device: string): Promise<WireguardDeviceCredentials> {
const url = urlJoin(WIREGUARD_API_URL, device);
const remoteConfigUrl = url;
const localConfigUrl = `${url}?local=true`;
const [configRemote, configLocal] = await Promise.all([
fetchWireguardConfigFile(remoteConfigUrl),
fetchWireguardConfigFile(localConfigUrl)
]);
this.ensureDeviceExists(device);

const configRemote = await this.fetchRemoteDeviceConfig(device);
const configLocal = await this.fetchLocalDeviceConfig(device);

return { configRemote, configLocal };
}

async getDeviceConfig(device: string, isLocal: boolean): Promise<string> {
this.ensureDeviceExists(device);
return isLocal ? this.fetchLocalDeviceConfig(device) : this.fetchRemoteDeviceConfig(device);
}

private ensureDeviceExists(device: string): void {
if (!this.getDevices().includes(device)) throw Error(`Device ${device} is not registered in WireGuard PEERS`);
}

private async fetchRemoteDeviceConfig(device: string): Promise<string> {
return fetchWireguardConfigFile(urlJoin(WIREGUARD_API_URL, device));
}

private async fetchLocalDeviceConfig(device: string): Promise<string> {
const url = `${urlJoin(WIREGUARD_API_URL, device)}?local=true`;

return fetchWireguardConfigFile(url).catch((e) => {
logs.warn(`Error fetching local WireGuard credentials for ${device}: ${e.message}`);
throw e;
});
}
}

// Utils
Expand Down Expand Up @@ -95,6 +118,10 @@ export async function wireguardDeviceGet(device: string): Promise<WireguardDevic
return wireguardClient.getDeviceCredentials(device);
}

export async function wireguardDeviceConfigGet({ device, isLocal }: { device: string; isLocal: boolean }): Promise<string> {
return wireguardClient.getDeviceConfig(device, isLocal);
}

export async function wireguardDevicesGet(): Promise<string[]> {
return wireguardClient.getDevices();
}
4 changes: 4 additions & 0 deletions packages/types/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,9 @@ export interface Routes {
/** Get credentials for a single Wireguard device */
wireguardDeviceGet(device: string): Promise<WireguardDeviceCredentials>;

/** Get a remote or local config for a single Wireguard device */
wireguardDeviceConfigGet(options: { device: string; isLocal: boolean }): Promise<string>;

/** Get URLs to a single Wireguard credentials */
wireguardDevicesGet(): Promise<string[]>;
}
Expand Down Expand Up @@ -1040,6 +1043,7 @@ export const routesData: { [P in keyof Routes]: RouteData } = {
wireguardDeviceAdd: { log: true },
wireguardDeviceRemove: { log: true },
wireguardDeviceGet: {},
wireguardDeviceConfigGet: {},
wireguardDevicesGet: {}
};

Expand Down
Loading