Skip to content

Commit

Permalink
Simple IP range migration (#246)
Browse files Browse the repository at this point in the history
* Fix gitignore

* Add global envs

* Fix test

* Set alias for dappmanager API

* Remove hardcoded IP

* Hardcode config for new IP range

* Fix route

* Generate config depending on IP range

* remove isAdmin middleware

* add minimumDappnodeVersion

* fix typo

* fix typo

* Fix non-existing vars file

* Improved dns IP resolution

---------

Co-authored-by: pablomendezroyo <[email protected]>
  • Loading branch information
dappnodedev and pablomendezroyo authored Jan 12, 2024
1 parent 357ca24 commit 4465106
Show file tree
Hide file tree
Showing 16 changed files with 143 additions and 115 deletions.
81 changes: 6 additions & 75 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,78 +1,9 @@
# dedicated files
build_*
build/src/node_modules/
build/src/mockFiles/
build/src/db.json
build/src/vpndb/
build/src/vpndb.json
uploader

# Build artifacts
build/src/dist
build/ui_openvpn/build

# Version data file
.version.json
.git-data.json

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components
releases.json

# node-waf configuration
.lock-wscript
ui_openvpn/node_modules
ui_openvpn/build

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
src/node_modules
src/dist

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next
build_*
4 changes: 2 additions & 2 deletions bin/ovpn_genconfig
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,10 @@ set -u

# Clean file to not concatenate config
# Syntax `true > $PATH` deletes the contents of $PATH without deleting the file
true > $OVPN_ENV
true >$OVPN_ENV

(set | grep '^OVPN_') | while read -r var; do
echo "declare -x $var" >> "$OVPN_ENV"
echo "declare -x $var" >>"$OVPN_ENV"
done

conf=${OPENVPN:-}/openvpn.conf
Expand Down
13 changes: 13 additions & 0 deletions bin/ovpn_initpki
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@ source "$OPENVPN/ovpn_env.sh"

nopass=$1

# The vars file is used to set the default values for the certificate fields. It is necessary to run the init-pki.
if [ -z "$EASYRSA_VARS_FILE" ]; then
EASYRSA_VARS_FILE="$OPENVPN/vars"
fi

if [ ! -f "$EASYRSA_VARS_FILE" ]; then
if [ -f "/usr/share/easy-rsa/vars.example" ]; then
cp /usr/share/easy-rsa/vars.example "$EASYRSA_VARS_FILE"
else
touch "$EASYRSA_VARS_FILE"
fi
fi

# Provides a sufficient warning before erasing pre-existing files
easyrsa init-pki

Expand Down
24 changes: 13 additions & 11 deletions dappnode_package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,15 @@
"version": "0.2.8",
"description": "Dappnode package responsible for providing the VPN (OpenVPN) connection",
"type": "dncore",
"architectures": [
"linux/amd64",
"linux/arm64"
],
"architectures": ["linux/amd64", "linux/arm64"],
"author": "DAppNode Association <[email protected]> (https://github.com/dappnode)",
"contributors": [
"Eduardo Antuña <[email protected]> (https://github.com/eduadiez)",
"DAppLion <[email protected]> (https://github.com/dapplion)",
"vdo <[email protected]> (https://github.com/vdo)",
"Alex Floyd <[email protected]> (https://github.com/mex20)"
],
"keywords": [
"DAppNodeCore",
"VPN",
"OpenVPN"
],
"keywords": ["DAppNodeCore", "VPN", "OpenVPN"],
"links": {
"homepage": "https://github.com/dappnode/DNP_VPN#readme"
},
Expand All @@ -29,5 +22,14 @@
"bugs": {
"url": "https://github.com/dappnode/DNP_VPN/issues"
},
"license": "GPL-3.0"
}
"license": "GPL-3.0",
"globalEnvs": [
{
"envs": ["HOSTNAME", "INTERNAL_IP"],
"services": ["vpn.dnp.dappnode.eth"]
}
],
"requirements": {
"minimumDappnodeVersion": "0.2.85"
}
}
2 changes: 0 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ services:
ports:
- "1194:1194/udp"
- "8092:8092"
dns: 172.33.1.2
networks:
dncore_network:
ipv4_address: 172.33.1.4
aliases:
- vpn.dappnode
logging:
Expand Down
8 changes: 7 additions & 1 deletion releases.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,11 @@
"dappnode": "Tue, 19 Nov 2019 17:32:32 GMT"
},
"link": "http://my.dappnode/#/sdk/publish/r=vpn.dnp.dappnode.eth&v=0.2.4&h=%2Fipfs%2FQmaP4xFLPXiNzcMCWoSboQZgrDLicrohs7SbfaFcJJMEPD"
},
"0.2.8": {
"hash": "/ipfs/QmdnAPxx8vm4w92vGUPBhMrNDALd6EGVc8JrZAzDKRzBWR",
"uploadedTo": {
"http://10.20.0.16:5001": "Thu, 11 Jan 2024 15:43:20 GMT"
}
}
}
}
2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@types/ip": "^1.1.0",
"@types/lodash": "^4.14.157",
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.14",
"@types/node": "^14.0.27",
"@types/prettyjson": "^0.0.29",
"@types/proxyquire": "^1.3.28",
"@types/sinon": "^9.0.4",
Expand Down
6 changes: 0 additions & 6 deletions src/src/api/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ function isLocalhostIp(ip: string): boolean {
return allowAllIps || localhostIps.some(_ip => ip.includes(_ip));
}

export const isAdmin: express.RequestHandler = (req, res, next) => {
const ip = req.ip;
if (isAdminIp(ip)) next();
else res.status(403).send(`Requires admin permission. Forbidden ip: ${ip}`);
};

export const isLocalhost: express.RequestHandler = (req, res, next) => {
const ip = req.ip;
if (isLocalhostIp(ip)) next();
Expand Down
4 changes: 2 additions & 2 deletions src/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getRpcHandler } from "./getRpcHandler";
import { logs } from "../logs";
import { LoggerMiddleware } from "../types";
import { wrapHandler } from "./utils";
import { isAdmin, isLocalhost } from "./auth";
import { isLocalhost } from "./auth";
import { clientConnect } from "./clientConnect";
import { CLIENT_CONNECT_PATHNAME } from "../params";

Expand All @@ -31,7 +31,7 @@ export function startHttpApi(port: number): void {
app.get("/", (_0, res) => res.send("VPN HTTP API"));

// Rest of RPC methods
app.post("/rpc", isAdmin, wrapHandler(rpcHandler));
app.post("/rpc", wrapHandler(rpcHandler));

// OpenVPN hooks
// Hook called by openvpn binary on each client connection
Expand Down
9 changes: 8 additions & 1 deletion src/src/dappmanager/fetchHostname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
NO_HOSTNAME_RETURNED_ERROR
} from "../params";
import { isDomain } from "../utils/domain";
import { logs } from "../logs";

/**
* Polls the DAPPMANAGER to get the HOSTNAME (domain or IP) necessary to start
Expand All @@ -22,7 +23,13 @@ export async function fetchHostname({
}): Promise<string> {
// If ENVs are already available, do not poll
const hostNameFromEnv = process.env[GLOBAL_ENVS.HOSTNAME];
if (hostNameFromEnv) return hostNameFromEnv;

if (hostNameFromEnv) {
logs.info(`Using hostname from ENV: ${hostNameFromEnv}`);
return hostNameFromEnv;
}

logs.info(`Fetching hostname from DAPPMANAGER: ${dappmanagerApiUrlGlobalEnvs}`);

// Add async-retry in case the DAPPMANAGER returns an 200 code with empty hostname
return await retry(
Expand Down
14 changes: 14 additions & 0 deletions src/src/dappmanager/fetchInternalIp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,21 @@ import { config } from "../config";

export async function getInternalIpCached(): Promise<string> {
// internal IP is an optional feature for when NAT-Loopback is off

const envInternalIp = process.env[GLOBAL_ENVS_KEYS.INTERNAL_IP];

if (envInternalIp && ip.isV4Format(envInternalIp)) {

logs.info(`Using internal IP from ENV: ${envInternalIp}`);

config.internalIp = envInternalIp;
return envInternalIp;
}

logs.info(`Fetching internal IP from DAPPMANAGER: ${dappmanagerApiUrlGlobalEnvs}`);

try {

const internalIp = await got(GLOBAL_ENVS_KEYS.INTERNAL_IP, {
throwHttpErrors: true,
prefixUrl: dappmanagerApiUrlGlobalEnvs
Expand Down
57 changes: 46 additions & 11 deletions src/src/openvpn/openvpnConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,71 @@ import fs from "fs";
import { shell, shellArgs } from "../utils/shell";
import { directoryIsEmptyOrEnoent } from "../utils/fs";
import { PKI_PATH, PROXY_ARP_PATH } from "../params";
import { logs } from "../logs";
import { getContainerIP } from "../utils/getDockerContainerIp";

type OvpnGenConfigFlags = {
c: string; // Enable traffic among the clients connected to the VPN (Boolean, no value)
d: string; // Disable default route (disables NAT without '-N'). Only specific traffic will go through the VPN (Boolean, no value)
u: string; // Hostname the clients will use to connect to the VPN
s: string; // Subnet the server will use to assign IPs to the clients
p: string; // Route to push to the client
n: string; // DNS server (BIND)
// There are more flags available, but we don't need them here
};

/**
* Initializes the OpenVPN configuration
* This function MUST be called before starting the openvpn binary
*/
export async function initalizeOpenVpnConfig(hostname: string): Promise<void> {
const vpnContainerDomain = "vpn.dappnode";
// Replicate environment used in entrypoint.sh
const openVpnEnv = {
OVPN_CN: hostname,
EASYRSA_REQ_CN: hostname
};
let genConfigFlags: OvpnGenConfigFlags;

// Initialize config and PKI
// -c: Client to Client
// -d: disable default route (disables NAT without '-N')
// -p "route 172.33.0.0 255.255.0.0": Route to push to the client
// -n "172.33.1.2": DNS server (BIND)
await shellArgs(
"ovpn_genconfig",
{
c: true,
d: true,
logs.info("Initializing OpenVPN configuration");

// Check current IP range
const containerIp = await getContainerIP(vpnContainerDomain);

// If container IP is inside 172.33.0.0/16 --> generate credentials A
if (containerIp && containerIp.startsWith("172.33.")) {
logs.info("Generating credentials for IP range 172.33.0.0/16");
genConfigFlags = {
c: "",
d: "",
u: `udp://"${hostname}"`,
s: "172.33.8.0/22",
p: `"route 172.33.0.0 255.255.0.0"`,
n: `"172.33.1.2"`
},
};

// Else (default, but it should be 10.20.0.0/24) --> generate credentials B
} else {
logs.info("Generating credentials for IP range 10.20.0.0/24");
genConfigFlags = {
c: "",
d: "",
u: `udp://"${hostname}"`,
s: "10.20.0.240/28",
p: `"route 10.20.0.0 255.255.255.0"`,
n: `"10.20.0.2"`
};
}

// Initialize config and PKI
const output = await shellArgs(
"ovpn_genconfig",
genConfigFlags,
{ env: { ...process.env, ...openVpnEnv } }
);

logs.info(`OpenVPN configuration output:\n\n${output}\n\n`);

// Check if PKI is initalized already, if not use hostname as CN
if (directoryIsEmptyOrEnoent(PKI_PATH))
await shell("ovpn_initpki nopass", {
Expand Down
2 changes: 1 addition & 1 deletion src/src/params.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from "path";

// DAPPMANAGER Params
export const dappmanagerApiUrl = "http://172.33.1.7";
export const dappmanagerApiUrl = "http://dappmanager.dappnode";
export const dappmanagerApiUrlGlobalEnvs = `${dappmanagerApiUrl}/global-envs`;

// OpenVPN parameters
Expand Down
23 changes: 23 additions & 0 deletions src/src/utils/getDockerContainerIp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Resolver } from 'dns';
import { logs } from '../logs';

export async function getContainerIP(containerName: string): Promise<string | null> {

const resolver = new Resolver();

// Use Docker's DNS server to resolve container name.
resolver.setServers(['127.0.0.11']);

return new Promise((resolve) => {
resolver.resolve4(containerName, (err, addresses) => {
if (err) {
logs.error(`Error resolving ${containerName} IP address: ${err}`);
resolve(null);
} else {
// Resolve with the first address found (if any).
logs.info(`Resolved ${containerName} IP addresses: ${addresses}`);
resolve(addresses.length > 0 ? addresses[0] : null);
}
});
});
}
2 changes: 1 addition & 1 deletion src/test/client.test.int.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe.skip("Integration test", () => {
throw Error("OpenVPN binaries were not found in /usr/local/bin/");
}
try {
await shell("ovpn_genconfig -c -d -u udp://test -s 172.33.8.0/23");
await shell("ovpn_genconfig -c -d -u udp://test -s 10.20.0.240/28");
await shell("EASYRSA_REQ_CN=test ovpn_initpki nopass");
await shell("easyrsa build-client-full dappnode_admin nopass");
await shell("easyrsa build-client-full luser nopass");
Expand Down
Loading

0 comments on commit 4465106

Please sign in to comment.