Skip to content

Commit

Permalink
Initial server roles architecture
Browse files Browse the repository at this point in the history
  • Loading branch information
Seshpenguin committed Mar 17, 2024
1 parent 28eeef8 commit 0205f93
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 18 deletions.
5 changes: 4 additions & 1 deletion distro-files/layout/opt/prolinux-server/setup-bridge.sh
@@ -1,5 +1,7 @@
#!/usr/bin/env bash

# arg $1 is the bridge interface MAC address

# First, let's stop NetworkManager to manually manage the network interfaces.
echo "Stopping NetworkManager..."
sudo systemctl stop NetworkManager
Expand All @@ -9,8 +11,9 @@ bridgeInterface="br0"

# Check if the bridge interface already exists, if not, create it
if ! ip link show $bridgeInterface > /dev/null 2>&1; then
echo "Creating bridge interface $bridgeInterface"
echo "Creating bridge interface $bridgeInterface with MAC address $1..."
ip link add name $bridgeInterface type bridge
ip link set dev $bridgeInterface address $1
else
echo "Bridge interface $bridgeInterface already exists"
fi
Expand Down
31 changes: 31 additions & 0 deletions ocs2-prolinuxd/cli-src/pl2/updater/updatercli.ts
Expand Up @@ -100,5 +100,36 @@ export async function registerPL2Commands(program: Command) {
}
console.log("Done! Please reboot now.");
});

program.command('hostname')
.argument('<hostname>', 'hostname')
.description('Set the hostname')
.action(async (str, options) => {
await callWS(LocalActions.SET_HOSTNAME, { hostname: str }, true);
console.log("Done! Please reboot now.");
});

const server = program.command('server').description('prolinux server role tools')
server.command('status').description('get the status of server roles').action(async () => {
const status = (await callWS(LocalActions.SERVER_STATUS, {}, true));
console.log("----------------------------------------");
console.log("Server Roles:");
console.log(JSON.stringify(status, null, 2));
console.log("----------------------------------------");
});
server.command('enable')
.argument('<role>', 'role')
.description('enable a server role')
.action(async (str, options) => {
await callWS(LocalActions.SERVER_ROLE_ENABLE, { role: str }, true);
console.log("Done! Please reboot now.");
});
server.command('disable')
.argument('<role>', 'role')
.description('disable a server role')
.action(async (str, options) => {
await callWS(LocalActions.SERVER_ROLE_DISABLE, { role: str }, true);
console.log("Done! Please reboot now.");
});
}
// sudo zsync http://espi.sineware.ca/repo/prolinux/mobile/dev/arm64/prolinux-root-mobile-dev.squish.zsync -o ~/prolinux_b.squish
37 changes: 36 additions & 1 deletion ocs2-prolinuxd/src/constants.ts
Expand Up @@ -18,6 +18,11 @@ export enum LocalActions {
DESCRIBE_API = "describe-api",
SET_REMOTE_API = "set-remote-api",
SET_RESET_PERSISTROOT_FLAG = "set-reset-persistroot-flag",
RUNTIME_VERIFY_STATE_INTEGRITY = "runtime-verify-state-integrity",
// Server actions
SERVER_STATUS = "server-status",
SERVER_ROLE_ENABLE = "server-role-enable",
SERVER_ROLE_DISABLE = "server-role-disable",
}
export interface LocalWSMessage {
action: LocalActions,
Expand Down Expand Up @@ -48,4 +53,34 @@ export interface RemoteUpdate {
buildstring: string,
url: string,
jwt: string
}
}

/* ProLinux Server Roles */
export enum ServerRoleType {
WEBSERVER = "webserver",
SECURE_SWITCH_APPLIANCE = "secure-switch-appliance"
}
export interface ServerRole<T> {
name: ServerRoleType,
description: string,
enabled: boolean
config: T
}
export type ServerRoleWebserverConfig = {
port: number,
root: string,
index: string,
ssl: boolean,
ssl_cert: string,
ssl_key: string
};
export type ServerRoleSecureSwitchConfig = {
interfaces: string[],
bridge_mac: string,
dhcp: boolean,
ip: string,
netmask: string,
gateway: string,
dns: string,
};

10 changes: 7 additions & 3 deletions ocs2-prolinuxd/src/helpers/runCmd.ts
@@ -1,17 +1,21 @@
import { spawn } from "child_process";
import {log} from "../logging";

export async function runCmd(cmd: string, args: string[]): Promise<string> {
export async function runCmd(cmd: string, args: string[], streamStdout: boolean = false, timeout: number = 30000): Promise<string> {
log.info(`About to exec: ${cmd} ${args.join(" ")}`);
return new Promise((resolve, reject) => {
const proc = spawn(cmd, args);
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data.toString();
if (streamStdout)
log.info(data.toString());
});
proc.stderr.on("data", (data) => {
stderr += data.toString();
if (streamStdout)
log.error(data.toString());
});
proc.on("close", (code) => {
if (code === 0) {
Expand All @@ -21,10 +25,10 @@ export async function runCmd(cmd: string, args: string[]): Promise<string> {
}
});

// Timeout after 10 seconds
// Timeout after 30 seconds
setTimeout(() => {
proc.kill();
reject(new Error(`Command ${cmd} ${args.join(" ")} timed out`));
}, 10000);
}, timeout);
});
}
30 changes: 26 additions & 4 deletions ocs2-prolinuxd/src/index.ts
Expand Up @@ -25,23 +25,37 @@ export const localSocketBroadcast = (msg: LocalWSMessage) => {
}
});
}
let cleanupFunctions = [] as (() => void)[];

async function main() {
// Read configuration file
log.info("Loading base system configuration...");
try {
const tomlConfig = TOML.parse(fs.readFileSync(process.env.CONFIG_FILE ?? path.join(__dirname, "prolinux.toml"), "utf-8")) as typeof state.config;
state.config = deepExtend(state.config, tomlConfig);
log.info("Configuration file loaded!");
log.info(JSON.stringify(state.config, null, 4));
} catch(e) {
} catch(e: any) {
console.log(e);
console.log("Resetting to default configuration file...");
log.error("Could not load configuration file, resetting to default... " + e?.message);
let tomlConfig = TOML.stringify(state.config, {
newline: "\n"
});
// todo check for a prolinux-default.toml
fs.writeFileSync(process.env.CONFIG_FILE ?? path.join(__dirname, "prolinux.toml"), Buffer.from(tomlConfig), "utf-8");
}
// Read extra-config json file
log.info("Loading extra configuration...");
try {
const extraConfig = JSON.parse(fs.readFileSync(path.join(path.dirname(process.env.CONFIG_FILE ?? path.join(__dirname, "prolinux.toml")), "extra-config.json"), "utf-8")) as typeof state.extraConfig;
state.extraConfig = deepExtend(state.extraConfig, extraConfig);
log.info("Extra configuration file loaded!");
log.info(JSON.stringify(state.extraConfig, null, 4));
} catch(e: any) {
console.log(e);
log.error("Could not load extra configuration file, resetting to default... " + e?.message);
let extraConfig = JSON.stringify(state.extraConfig, null, 4);
fs.writeFileSync(path.join(path.dirname(process.env.CONFIG_FILE ?? path.join(__dirname, "prolinux.toml")), "extra-config.json"), Buffer.from(extraConfig), "utf-8");
}

try{
fs.unlinkSync("/tmp/prolinuxd.sock");
Expand Down Expand Up @@ -87,7 +101,8 @@ async function main() {
}
if(state.config.prolinuxd.modules.includes("pl2")) {
log.info("Starting ProLinux 2 Module...");
await loadPL2Module();
let cleanup = await loadPL2Module();
cleanupFunctions.push(cleanup);
}
if(state.config.pl2.remote_api) {
// create a new wss server on port 25567 using wsConnectionHandler
Expand All @@ -109,6 +124,13 @@ async function main() {
});
}
try {
// setup cleanup on exit
process.on("exit", () => {
log.info("Shutting down ProLinuxD...");
cleanupFunctions.forEach((cleanup) => {
cleanup();
});
});
main();
} catch (err) {
log.error("Fatal error: " + err);
Expand Down
2 changes: 1 addition & 1 deletion ocs2-prolinuxd/src/logging.ts
Expand Up @@ -19,7 +19,7 @@ export function logger(msg: string, type: string, from: string = "prolinuxd") {
}));
} else {
// todo plasma-mobile-nightly only
fs.appendFileSync("/dev/tty1", `[prolinuxd] [${type}] ${msg}\n`);
//fs.appendFileSync("/dev/tty1", `[prolinuxd] [${type}] ${msg}\n`);
}
console.log(`[prolinuxd] [${type}] ${msg}`);
}
Expand Down
25 changes: 23 additions & 2 deletions ocs2-prolinuxd/src/modules/pl2/index.ts
Expand Up @@ -3,6 +3,7 @@ import { state } from "../../state/systemStateContainer";
import { runCmd } from "../../helpers/runCmd";
import { log } from "../../logging";
import { getProLinuxInfo } from "../../helpers/getProLinuxInfo";
import { startSecureSwitchRole, stopSecureSwichRole } from "./server/secureSwitchRole";

const config = state.config;

Expand Down Expand Up @@ -65,7 +66,7 @@ async function startPasswordService() {
}

// Watches /etc/shadow for password changes, and persists them to the config
fs.watch("/etc/shadow", async (eventType, filename) => {
state.untracked.passwordServiceWatcher = fs.watch("/etc/shadow", async (eventType, filename) => {
log.info("Shadow file (password) updated: " + eventType + ", " + filename);
const shadow = await fs.promises.readFile("/etc/shadow", "utf-8");
const user_shadow = shadow.split("\n").filter((line) => {
Expand All @@ -79,7 +80,7 @@ async function startNMNetworksService() {
// this function is similar to the password service, in that it watches for changes to the network configuration
// but here we instead copy the file to /sineware/data/customization/etc/NetworkManager/system-connections
log.info("Starting NetworkManager networks sync service...");
fs.watch("/etc/NetworkManager/system-connections", async (eventType, filename) => {
state.untracked.NMNetworksServiceWatcher = fs.watch("/etc/NetworkManager/system-connections", async (eventType, filename) => {
log.info("NetworkManager configuration updated: " + eventType + ", " + filename);
const network = await fs.promises.readFile("/etc/NetworkManager/system-connections/" + filename, "utf-8");
await fs.promises.writeFile(`/sineware/data/customization/etc/NetworkManager/system-connections/${filename}`, network);
Expand All @@ -92,4 +93,24 @@ export async function loadPL2Module() {
await startDeviceSpecificServices();
await startPasswordService();
await startNMNetworksService();

if(state.extraConfig.server_roles.webserver?.enabled) {
log.info("Starting Webserver Server Role...");
log.info("Stub: todo");
// Start webserver
}
if(state.extraConfig.server_roles.secure_switch?.enabled) {
try {
await startSecureSwitchRole();
} catch(e: any) {
log.error("Failed to start SecureSwitch Appliance Server Role: " + e.message);
}
}
log.info("ProLinux 2 Module loaded!");

// return cleanup function
return async () => {
log.info("Cleaning up PL2 Module...");
await stopSecureSwichRole();
}
}
97 changes: 97 additions & 0 deletions ocs2-prolinuxd/src/modules/pl2/server/secureSwitchRole.ts
@@ -0,0 +1,97 @@
import isReachable from "is-reachable";
import { runCmd } from "../../../helpers/runCmd";
import { log } from "../../../logging";
import { ServerRoleType } from "../../../constants";
import { state } from "../../../state/systemStateContainer";

const SURICATA_CONTAINER_NAME = `PLINTERNAL_${ServerRoleType.SECURE_SWITCH_APPLIANCE}_surciata`;
const SURICATA_IMAGE_NAME = "docker.io/jasonish/suricata:latest@sha256:01e8d513beb284c8738ce0fbd98a8e95d202f1e27a04c15b00c1c61c3a2b8fdc";

function generateMACAddress(): string {
const hexDigits = "0123456789ABCDEF";
let macAddress = "52:54:00"; // Using the QEMU VM OUI space
for (let i = 0; i < 6; i++) {
if (i % 2 === 0) macAddress += ":";
macAddress += hexDigits.charAt(Math.floor(Math.random() * 16));
}
return macAddress;
}

// this function creates the podman container SURIATA_CONTAINER_NAME
export async function setupSecureSwitchRole(): Promise<boolean> {
log.info("[Server] [SecureSwitch] Setting up Suricata container...");
try {
await runCmd("podman", ["inspect", SURICATA_CONTAINER_NAME]);
log.info("[Server] [SecureSwitch] Suricata container already exists!");
return false;
} catch(e: any) {
log.info("[Server] [SecureSwitch] Suricata container does not exist, creating...");
// create but don't start the container
/* docker run --rm -it --net=host \
--cap-add=net_admin --cap-add=net_raw --cap-add=sys_nice \
jasonish/suricata:latest -i <interface>*/
await runCmd("podman", ["create", "--name", SURICATA_CONTAINER_NAME, "--net=host", "--cap-add=net_admin", "--cap-add=net_raw", "--cap-add=sys_nice", SURICATA_IMAGE_NAME, "-i", "br0"]);

// generate a linux MAC Address for the bridge using the following format: 00:00:00:00:00:00
const bridgeMac = generateMACAddress();
log.info("[Server] [SecureSwitch] Generated MAC Address for bridge: " + bridgeMac);
state.extraConfig.server_roles.secure_switch.config.bridge_mac = bridgeMac;
return true;
}
}
export async function deleteSecureSwitchRole() {
log.info("[Server] [SecureSwitch] Deleting Suricata container...");
try {
await runCmd("podman", ["rm", SURICATA_CONTAINER_NAME]);
} catch(e: any) {
log.error("[Server] [SecureSwitch] Failed to delete Suricata container: " + e.message);
}
}

export async function startSecureSwitchRole() {
// the bridge setup script is in /opt/prolinux-server/setup-bridge.sh
log.info("[Server] [SecureSwitch] Starting SecureSwitch Appliance Server Role...");
await runCmd("/opt/prolinux-server/setup-bridge.sh", [state.extraConfig.server_roles.secure_switch.config.bridge_mac], true, 3600000);

// the suricata container exists under the podman name PLINTERNAL_${ServerRoleType.SECURE_SWITCH_APPLIANCE}_surciata
// check if it exists and start it
let suricataExists = false;
try {
await runCmd("podman", ["inspect", SURICATA_CONTAINER_NAME]);
suricataExists = true;
} catch(e: any) {
log.error("[Server] [SecureSwitch] Suricata container does not exist! Failed to start SecureSwitch Appliance Server Role: " + e.message);
return;
}
if(suricataExists) {
log.info("[Server] [SecureSwitch] Starting Suricata container...");
// we need to check if the container image is the version specified in SURICATA_IMAGE_NAME. If it's not, wait for the network to come up using isReachable, then pull the new image and start the container
const suricataVersion = await runCmd("podman", ["inspect", "--format", "{{.ImageName}}", SURICATA_CONTAINER_NAME], true);
if(suricataVersion !== SURICATA_IMAGE_NAME) {
// check if the network is up or timeout after 1 minute
log.info("[Server] [SecureSwitch] Post-update, pulling new Suricata image...");
log.info("Waiting for network to come up...")
let networkUp = false;
for(let i = 0; i < 6; i++) {
networkUp = await isReachable("update.sineware.ca");
if(networkUp) {
log.info("Network is up!");
break;
}
log.info("Network is down, retrying in 10 seconds...");
await new Promise((resolve) => setTimeout(resolve, 10000));
}
await runCmd("podman", ["pull", SURICATA_IMAGE_NAME], true, 3600000);
// delete the old container and run setupSuricataContainer again
await runCmd("podman", ["rm", SURICATA_CONTAINER_NAME], true);
if(await setupSecureSwitchRole()) {
await runCmd("podman", ["start", SURICATA_CONTAINER_NAME], true);
}
} else {
await runCmd("podman", ["start", SURICATA_CONTAINER_NAME], true);
}
}
}
export async function stopSecureSwichRole() {

}

0 comments on commit 0205f93

Please sign in to comment.