diff --git a/apps/Dockerfile b/apps/Dockerfile index fcfaaf6..633e70c 100644 --- a/apps/Dockerfile +++ b/apps/Dockerfile @@ -24,5 +24,6 @@ COPY --chown=jager:jager analyzer.py . COPY --chown=jager:jager intrusion_detection.py . COPY --chown=jager:jager utils.py . COPY --chown=jager:jager coco.labels . +COPY --chown=jager:jager events.py . CMD python3 analyzer.py diff --git a/deploy/templates/docker-compose.jin b/deploy/templates/docker-compose.jin index 019a186..4af7e0b 100644 --- a/deploy/templates/docker-compose.jin +++ b/deploy/templates/docker-compose.jin @@ -11,6 +11,10 @@ services: {%- endif %} image: "jagereye/api:{{services.api.version}}" network_mode: "{{services.api.network_mode}}" + cap_add: + {%- for cap in services.api.cap_add %} + - "{{cap}}" + {%- endfor %} ports: {%- for _, port in services.api.ports.items() %} - "{{port}}" diff --git a/services/api/Dockerfile b/services/api/Dockerfile index 2861865..f372e02 100644 --- a/services/api/Dockerfile +++ b/services/api/Dockerfile @@ -7,9 +7,11 @@ WORKDIR ${HOME} # Install ffmpeg. USER root RUN apt-get update && apt-get install -y --no-install-recommends \ - ffmpeg && \ + ffmpeg net-tools sudo isc-dhcp-client iproute && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* + +COPY --chown=root:root ./settings_utils/sudoers /etc/ USER jager # Create services structure diff --git a/services/api/app.js b/services/api/app.js index 1fc4596..1ae181b 100644 --- a/services/api/app.js +++ b/services/api/app.js @@ -8,6 +8,7 @@ const { users, createAdminUser } = require('./users') const analyzers = require('./analyzers') const events = require('./events') const helpers = require('./helpers') +const { settings, createDefaultNetworkSetting } = require('./settings') const app = express() @@ -22,6 +23,7 @@ app.use(API_ENTRY, users) app.use(API_ENTRY, analyzers) app.use(API_ENTRY, events) app.use(API_ENTRY, helpers) +app.use(API_ENTRY, settings) // Logging errors app.use((err, req, res, next) => { @@ -40,5 +42,6 @@ app.use((err, req, res, next) => { // Create admin user if it is not existed createAdminUser() +createDefaultNetworkSetting() module.exports = app diff --git a/services/api/settings.js b/services/api/settings.js new file mode 100644 index 0000000..9eabb51 --- /dev/null +++ b/services/api/settings.js @@ -0,0 +1,166 @@ +const express = require('express'); +const router = express.Router(); +const Ajv = require('ajv'); +const P = require('bluebird'); + +const config = require('./config').services.api.settings; +const models = require('./database'); +const settingsModel = P.promisifyAll(models['settings']); +const { createError } = require('./utils'); +const { routesWithAuth } = require('./auth'); +const { resetNetworkInterface, getInterfaceIp, ResetNetworkError } = require('./settings_utils'); + +const dataPortInterface = config.data_port.device; + +const ajv = new Ajv(); +const settingPatchSchema = { + 'anyOf': [ + { + type: 'object', + properties: { + mode: { + type: 'string', + pattern: '^static$', + }, + address: { + type: 'string', + format: 'ipv4' + }, + gateway: { + type: 'string', + format: 'ipv4' + }, + netmask: { + type: 'string', + format: 'ipv4' + } + }, + additionalProperties: false, + required: ['mode', 'address', 'gateway', 'netmask'] + }, + { + type: 'object', + properties: { + mode: { + type: 'string', + pattern: '^dhcp$', + }, + }, + additionalProperties: false, + required: ['mode'] + }, + ] +} + +const settingPatchValidator = ajv.compile(settingPatchSchema); + +function validateSettingPatch(req, res, next) { + if(!settingPatchValidator(req.body)) { + // TODO: returned msg should be refine + return next(createError(400, settingPatchValidator.errors)); + } + next(); +} + +async function patchSettings(req, res, next) { + let query = {} + let body = req.body + + let mode = body.mode; + let addr = body.address; + let netmask = body.netmask; + let gateway = body.gateway; + + let newSettings = {}; + newSettings.mode = mode; + newSettings.address = addr; + newSettings.netmask = netmask; + newSettings.gateway = gateway; + newSettings.status = 'processing'; + // First, insert record in db and response + await settingsModel.updateAsync({'_id': 1}, newSettings, {'upsert': true}); + res.status(200).send(); + // start configure network interface + try { + await resetNetworkInterface(dataPortInterface, mode, addr, netmask, gateway); + // update the status in db + newSettings.status = 'done'; + // get the dhcp ip and update + if (mode === 'dhcp') { + let dhcp_address = getInterfaceIp(dataPortInterface); + if (!dhcp_address) { + throw new ResetNetworkError('dhcp failed'); + } + newSettings.address = dhcp_address; + } + await settingsModel.updateAsync({'_id': 1}, newSettings, {'upsert': true}); + } catch (err) { + newSettings.status = 'failed'; + await settingsModel.updateAsync({'_id': 1}, newSettings, {'upsert': true}); + // TODO: logging + console.error(err); + }; +} + +async function getSettings(req, res, next) { + let result = await settingsModel.findOne({'_id': 1}); + if(result.mode === 'dhcp') { + result.netmask = undefined; + result.gateway = undefined; + + // Ray: when users set dhcp without port connected, + // the port cannot get IP. + // whenever user connect the port, the port will receive IP soon. + // then the status of the port should be update + + // on the other hand, whenever the port disconnected, + // the dhcp ip will be invalid, then the status should be updated + + let dhcp_address = getInterfaceIp(dataPortInterface); + if (dhcp_address) { + result.address = dhcp_address; + result.status = 'done'; + await settingsModel.updateAsync({'_id': 1}, result, {'upsert': true}); + } + else { + result.address = 'None'; + result.status = 'failed'; + } + } + res.status(200).send(result); +} + +async function createDefaultNetworkSetting() { + let result = await settingsModel.findOne({'_id': 1}); + if(!result) { + // create default network setting + let defaultSettings = {}; + defaultSettings.mode = 'dhcp'; + defaultSettings.address = 'None'; + defaultSettings.status = 'processing'; + try { + await settingsModel.updateAsync({'_id': 1}, defaultSettings, {'upsert': true}); + await resetNetworkInterface(dataPortInterface ,'dhcp'); + } catch (err) { + defaultSettings.status = 'failed'; + await settingsModel.updateAsync({'_id': 1}, defaultSettings, {'upsert': true}); + // TODO: logging + console.error(err) + } + } +} + + +/* + * Routing Table + */ +routesWithAuth( + router, + ['get', '/settings/networking', getSettings], + ['patch', '/settings/networking', validateSettingPatch, patchSettings], +) + +module.exports = { + settings: router, + createDefaultNetworkSetting +} diff --git a/services/api/settings_utils/index.js b/services/api/settings_utils/index.js new file mode 100644 index 0000000..8390504 --- /dev/null +++ b/services/api/settings_utils/index.js @@ -0,0 +1,3 @@ +const resetNetworkInterface = require('./resetNetworkInterface.js') + +module.exports = resetNetworkInterface; diff --git a/services/api/settings_utils/resetNetworkInterface.js b/services/api/settings_utils/resetNetworkInterface.js new file mode 100644 index 0000000..e4c6c0d --- /dev/null +++ b/services/api/settings_utils/resetNetworkInterface.js @@ -0,0 +1,53 @@ +const os = require('os') +const P = require('bluebird'); +const TimeoutError = P.TimeoutError; +const fs = P.promisifyAll(require('fs')); +const execAsync = P.promisify(require('child_process').exec); +const format = require('util').format; + +const dataportInterfaceFilename = 'jagereye_dataport_interface'; +const shellTimeout = 4000 + +class ResetNetworkError extends Error { + constructor (message, status) { + super(message); + this.name = this.constructor.name; + // Capturing stack trace, excluding constructor call from it. + Error.captureStackTrace(this, this.constructor); + this.status = status || 500; + } +}; + +async function resetNetworkInterface(networkInterface, mode, address, netmask, gateway) { + // flush the dataport ip + await execAsync(format('sudo ip addr flush dev %s', networkInterface), {timeout: shellTimeout}); + if(mode === 'static') { + await execAsync(format('sudo ifconfig %s %s', networkInterface, address), {timeout: shellTimeout}); + } + else if(mode === 'dhcp') { + try { + await execAsync(format('sudo dhclient %s', networkInterface), {timeout: shellTimeout}); + } catch(e){ + // it happened when the dataport cannot find out dhcp server + console.error(e); + throw new ResetNetworkError('dhcp failed'); + } + } +} + +function getInterfaceIp(interfaceName) { + let interfaces = os.networkInterfaces(); + try { + let ip = interfaces[interfaceName][0]['address']; + return ip; + } catch(e) { + // TODO: logging + return; + } +} + +module.exports = { + resetNetworkInterface: resetNetworkInterface, + getInterfaceIp: getInterfaceIp, + ResetNetworkError: ResetNetworkError +}; diff --git a/services/api/settings_utils/sudoers b/services/api/settings_utils/sudoers new file mode 100644 index 0000000..db99e9b --- /dev/null +++ b/services/api/settings_utils/sudoers @@ -0,0 +1,34 @@ +# +# This file MUST be edited with the 'visudo' command as root. +# +# Please consider adding local content in /etc/sudoers.d/ instead of +# directly modifying this file. +# +# See the man page for details on how to write a sudoers file. +# +Defaults env_reset +Defaults mail_badpass +Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" + +# Host alias specification + +# User alias specification + +# Cmnd alias specification +Cmnd_Alias FLUSH_IP = /sbin/ip addr flush dev en* +Cmnd_Alias IFCONFIG = /sbin/ifconfig en* * +Cmnd_Alias DHCLIENT = /sbin/dhclient en* + +# User privilege specification +root ALL=(ALL:ALL) ALL + +# Members of the admin group may gain root privileges +%admin ALL=(ALL) ALL + +# Allow members of group sudo to execute any command +%sudo ALL=(ALL:ALL) ALL + +# See sudoers(5) for more information on "#include" directives: + +#includedir /etc/sudoers.d +jager ALL=(ALL) NOPASSWD: FLUSH_IP, IFCONFIG, DHCLIENT diff --git a/shared/config.development.yml b/shared/config.development.yml index cefc853..052af26 100644 --- a/shared/config.development.yml +++ b/shared/config.development.yml @@ -3,6 +3,8 @@ services: api: version: "0.0.1" network_mode: host + cap_add: + - NET_ADMIN ports: client: "5000" base_url: "api/v1" @@ -12,6 +14,12 @@ services: token: enabled: true secret: "jagereye_dev" + settings: + control_port: + device: "enp1s0" + static_ip: "192.168.20.1" + data_port: + device: "enp2s0" database: version: "mongo-3.6.0" db_name: jagereye-dev diff --git a/shared/database.json b/shared/database.json index f1f3594..e68bdb9 100644 --- a/shared/database.json +++ b/shared/database.json @@ -51,6 +51,17 @@ "source": "SUBDOC_SOURCE", "pipelines": [ "SUBDOC_PIPELINES" ] }, + "settings": { + "_id": "Number", + "mode": { + "type": "string", + "required": true + }, + "address": "string", + "netmask": "string", + "gateway": "string", + "status": "string" + }, "events": { "type": { "type": "string",