Add mdns and ppp services.
This commit is contained in:
commit
26b2d1e6a8
11 changed files with 5073 additions and 0 deletions
37
docker-compose.yml
Normal file
37
docker-compose.yml
Normal file
|
@ -0,0 +1,37 @@
|
|||
version: "2"
|
||||
|
||||
volumes:
|
||||
bash_history:
|
||||
|
||||
services:
|
||||
ppp:
|
||||
build: ./ppp
|
||||
privileged: true
|
||||
network_mode: host
|
||||
environment:
|
||||
HISTFILE: /bash_history/bash_history
|
||||
volumes:
|
||||
- bash_history:/bash_history
|
||||
|
||||
mdns:
|
||||
build: ./mdns
|
||||
network_mode: host
|
||||
depends_on:
|
||||
- ppp
|
||||
cap_add:
|
||||
- SYS_RESOURCE
|
||||
- SYS_ADMIN
|
||||
security_opt:
|
||||
- "apparmor:unconfined"
|
||||
tmpfs:
|
||||
- /run
|
||||
- /sys/fs/cgroup
|
||||
labels:
|
||||
io.balena.features.dbus: "1"
|
||||
io.balena.features.supervisor-api: "1"
|
||||
environment:
|
||||
MDNS_TLD: private
|
||||
INTERFACE: ppp0
|
||||
HISTFILE: /bash_history/bash_history
|
||||
volumes:
|
||||
- bash_history:/bash_history
|
32
mdns/Dockerfile.template
Normal file
32
mdns/Dockerfile.template
Normal file
|
@ -0,0 +1,32 @@
|
|||
FROM balenalib/%%BALENA_MACHINE_NAME%%-debian-node:10-buster-build as base
|
||||
|
||||
RUN install_packages libdbus-glib-1-dev
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copies the package.json first for better cache on later pushes
|
||||
COPY package.json package-lock.json /usr/src/app/
|
||||
|
||||
# Install the publisher
|
||||
RUN JOBS=MAX npm ci --unsafe-perm --production && npm cache clean --force && rm -rf /tmp/*
|
||||
|
||||
# Build service
|
||||
FROM base as build
|
||||
|
||||
RUN JOBS=MAX npm ci
|
||||
|
||||
COPY . /usr/src/app/
|
||||
|
||||
RUN JOBS=MAX npm run build
|
||||
|
||||
# Final image
|
||||
FROM balenalib/%%BALENA_MACHINE_NAME%%-debian-node:buster
|
||||
|
||||
# Copy built code
|
||||
COPY --from=build /usr/src/app/build /usr/src/app/build
|
||||
COPY --from=build /usr/src/app/bin /usr/src/app/bin
|
||||
COPY --from=base /usr/src/app/node_modules /usr/src/app/node_modules
|
||||
|
||||
ENV DBUS_SYSTEM_BUS_ADDRESS 'unix:path=/host/run/dbus/system_bus_socket'
|
||||
|
||||
CMD /usr/src/app/bin/balena-mdns-publisher
|
100
mdns/README.md
Normal file
100
mdns/README.md
Normal file
|
@ -0,0 +1,100 @@
|
|||
# balena-mdns-publisher
|
||||
|
||||
The MDNS publisher advertises a set of local IP addresses for a local network (`<tld>.local`) instance of the balena-on-balena (BoB) or OpenBalena (OB) services. The same IP address is used for all services in the BoB or OB instance.
|
||||
|
||||
This allows any machine on the same subnet that is not more than one hop from the publisher and BoB/OB instance to automatically be able to resolve the hostnames used for the instance (as long as the machine supports mDNS/DNS-SD, also known as 'ZeroConf' networking).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The host machine running the publisher service must be running an instance of the [Avahi](https://www.avahi.org/) daemon, which this service uses for address publishing.
|
||||
|
||||
Additionally, the service requires the ability to use `systemd` (ie. access to host `cgroups` or relevant `tmpfs` mount), and the host DBUS socket.
|
||||
|
||||
## Installation and Running
|
||||
|
||||
This service can be run under a Linux environment, either any standard distribution running docker, or a resinOS device. The two configurations require separate setup, however. A `docker-compose` service is used here to show how to configure the service, and each specific target.
|
||||
|
||||
### Docker/`docker-compose` Setup
|
||||
|
||||
Regardless of target, the service requires particular environment variables and access to the host network. The following `docker-compose` snippet shows the requirements for running the service:
|
||||
|
||||
```
|
||||
balena-mdns-publisher:
|
||||
image: 'balena/balena-mdns-publisher:master'
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- SYS_RESOURCE
|
||||
- SYS_ADMIN
|
||||
security_opt:
|
||||
- 'apparmor:unconfined'
|
||||
tmpfs:
|
||||
- /run
|
||||
- /sys/fs/cgroup
|
||||
environment:
|
||||
<See 'Environment Variables' section>
|
||||
```
|
||||
|
||||
#### Generic Linux Host
|
||||
|
||||
Additionally, for a generic Linux host running Avahi and Docker, the following should be included in the service definition to expose the DBUS socket to the correct place inside the service container:
|
||||
|
||||
```
|
||||
volumes:
|
||||
- /run/dbus/system_bus_socket:/host/run/dbus/system_bus_socket
|
||||
```
|
||||
|
||||
Alternatively you may change the in-container location of the DBUS socket, but you *must* set `DBUS_SESSION_BUS_ADDRESS` envvar to the same location value.
|
||||
|
||||
#### balenaOS Device
|
||||
|
||||
Should the target be a balenaOS device, then the following section should also be included to ensure that the Supervisor correctly exposes the relevant information to the service:
|
||||
```
|
||||
labels:
|
||||
io.balena.features.dbus: '1'
|
||||
io.balena.features.supervisor-api: '1'
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The mDNS publisher requires some additional environment variables be passed to it on execution to allow it to function correctly. These are
|
||||
|
||||
* `CONFD_BACKEND` - This should always be set to `ENV`
|
||||
* `MDNS_TLD` - This is the full Top Level Domain of the host being published
|
||||
* `MDNS_SUBDOMAINS` - An array of subdomains to publish host addresses for
|
||||
* `DBUS_SESSION_BUS_ADDRESS` - This must always be set to `unix:path=/host/run/dbus/system_bus_socket'` (unless the DBUS target is located elsewhere)
|
||||
* `INTERFACE` - The name of the host network interface to publish the subdomain addresses too. Under balenaOS, if this is not set, the Supervisor API will be used to determine the interface to use, and therefore is not required. If this *is* set, it will override the returned default interface
|
||||
* `MDNS_API_TOKEN` (optional) - Should Public URL exposure be required, then the shared API token for the Proxy service should be set using this key. The API will be queried every 20 seconds, and any new device with an exposed public URL will have its UUID published as a subdomain. Previously published UUIDs that no longer have a public URL will be deleted
|
||||
* `BALENA_ROOT_CA` (optional) - Should the certificate chain used for the BoB/OB instance be via a self-signed CA, this value should be a Base64 encoded version of the CA's PEM certificate
|
||||
|
||||
This allows the acquisition of the underlying DBUS socket, as well as the ability
|
||||
to run `systemd`.
|
||||
|
||||
## Example `docker-compose` Service
|
||||
|
||||
The following is an example of adding the balena mDNS publisher to a BoB instance running under balenaOS:
|
||||
|
||||
```
|
||||
balena-mdns-publisher:
|
||||
image: 'balena/balena-mdns-publisher:master'
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- SYS_RESOURCE
|
||||
- SYS_ADMIN
|
||||
security_opt:
|
||||
- 'apparmor:unconfined'
|
||||
tmpfs:
|
||||
- /run
|
||||
- /sys/fs/cgroup
|
||||
labels:
|
||||
io.balena.features.dbus: '1'
|
||||
io.balena.features.supervisor-api: '1'
|
||||
environment:
|
||||
CONFD_BACKEND: ENV
|
||||
MDNS_TLD: my.bob.local
|
||||
MDNS_SUBDOMAINS: >-
|
||||
["admin", "api", ...]
|
||||
MDNS_API_TOKEN: 1234567890abcdef
|
||||
DBUS_SESSION_BUS_ADDRESS: 'unix:path=/host/run/dbus/system_bus_socket'
|
||||
BALENA_ROOT_CA: >-
|
||||
1234567890abcdef
|
||||
```
|
2
mdns/bin/balena-mdns-publisher
Executable file
2
mdns/bin/balena-mdns-publisher
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env node
|
||||
require('../build/app');
|
4519
mdns/package-lock.json
generated
Normal file
4519
mdns/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
53
mdns/package.json
Normal file
53
mdns/package.json
Normal file
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"version": "1.6.6",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prettify": "prettier --config ./node_modules/resin-lint/config/.prettierrc --write \"src/**/*.ts\" \"typings/**/*.ts\"",
|
||||
"lint": "resin-lint --typescript src/ && tsc --noEmit",
|
||||
"start": "node build/app.js"
|
||||
},
|
||||
"author": "Heds Simons <heds@balena.io>",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/balena-io/balena-mdns-publisher.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/balena-io/balena-mdns-publisher/issues"
|
||||
},
|
||||
"nyc": {
|
||||
"extension": [
|
||||
".ts"
|
||||
]
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"bluebird": "^3.5.1",
|
||||
"dbus-native": "^0.4.0",
|
||||
"lodash": "^4.17.15",
|
||||
"request": "^2.88.0",
|
||||
"request-promise": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.134",
|
||||
"@types/node": "^10.14.4",
|
||||
"@types/request-promise": "^4.1.42",
|
||||
"husky": "^1.3.1",
|
||||
"lint-staged": "^8.1.5",
|
||||
"prettier": "^1.16.4",
|
||||
"resin-lint": "^3.0.1",
|
||||
"typescript": "^3.4.3"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged",
|
||||
"pre-push": "npm run lint"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.ts": [
|
||||
"prettier --config ./node_modules/resin-lint/config/.prettierrc --write",
|
||||
"resin-lint --typescript --no-prettier",
|
||||
"git add"
|
||||
]
|
||||
}
|
||||
}
|
269
mdns/src/app.ts
Normal file
269
mdns/src/app.ts
Normal file
|
@ -0,0 +1,269 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright (C) 2018-2019 Balena Ltd.
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import * as Bluebird from 'bluebird';
|
||||
import { Message, systemBus } from 'dbus-native';
|
||||
import * as _ from 'lodash';
|
||||
import * as os from 'os';
|
||||
import * as request from 'request-promise';
|
||||
|
||||
/**
|
||||
* Supervisor returned device details interface.
|
||||
*/
|
||||
interface HostDeviceDetails {
|
||||
api_port: string;
|
||||
ip_address: string;
|
||||
os_version: string;
|
||||
supervisor_version: string;
|
||||
update_pending: boolean;
|
||||
update_failed: boolean;
|
||||
update_downloaded: boolean;
|
||||
commit: string;
|
||||
status: string;
|
||||
download_progress: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supervisor returned device name interface.
|
||||
*/
|
||||
interface DeviceNameRespose {
|
||||
status: string;
|
||||
deviceName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts published via Avahi.
|
||||
*/
|
||||
interface PublishedHosts {
|
||||
/** The Avahi group used to publish the host */
|
||||
group: string;
|
||||
/** The full hostname of the published host */
|
||||
hostname: string;
|
||||
/** The IP address of the published host */
|
||||
address: string;
|
||||
}
|
||||
|
||||
/** List of published hosts */
|
||||
const publishedHosts: PublishedHosts[] = [];
|
||||
|
||||
/** DBus controller */
|
||||
const dbus = systemBus();
|
||||
/**
|
||||
* DBus invoker.
|
||||
*
|
||||
* @param message DBus message to send
|
||||
*/
|
||||
const dbusInvoker = (message: Message): PromiseLike<any> => {
|
||||
return Bluebird.fromCallback(cb => {
|
||||
return dbus.invoke(message, cb);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the IPv4 address for the named interface.
|
||||
*
|
||||
* @param intf Name of interface to query
|
||||
*/
|
||||
const getNamedInterfaceAddr = (intf: string): string => {
|
||||
const nics = os.networkInterfaces()[intf];
|
||||
|
||||
if (!nics) {
|
||||
throw new Error('The configured interface is not present, exiting');
|
||||
}
|
||||
|
||||
// We need to look for the IPv4 address
|
||||
let ipv4Intf;
|
||||
for (const nic of nics) {
|
||||
if (nic.family === 'IPv4') {
|
||||
ipv4Intf = nic;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ipv4Intf) {
|
||||
throw new Error(
|
||||
'IPv4 version of configured interface is not present, exiting',
|
||||
);
|
||||
}
|
||||
|
||||
return ipv4Intf.address;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the IPv4 address for the default balena internet-connected interface.
|
||||
*/
|
||||
const getDefaultInterfaceAddr = async (): Promise<string> => {
|
||||
let deviceDetails: HostDeviceDetails | null = null;
|
||||
|
||||
// We continue to attempt to get the default IP address every 10 seconds,
|
||||
// inifinitely, as without our service the rest won't work.
|
||||
while (!deviceDetails) {
|
||||
try {
|
||||
deviceDetails = await request({
|
||||
uri: `${process.env.BALENA_SUPERVISOR_ADDRESS}/v1/device?apikey=${
|
||||
process.env.BALENA_SUPERVISOR_API_KEY
|
||||
}`,
|
||||
json: true,
|
||||
method: 'GET',
|
||||
}).promise();
|
||||
} catch (_err) {
|
||||
console.log(
|
||||
'Could not acquire IP address from Supervisor, retrying in 10 seconds',
|
||||
);
|
||||
await Bluebird.delay(10000);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that we only use the first returned IP address route. We don't want to broadcast
|
||||
// on multiple subnets.
|
||||
return deviceDetails.ip_address.split(' ')[0];
|
||||
};
|
||||
|
||||
const getDeviceName = async (): Promise<string> => {
|
||||
let deviceNameResponse: DeviceNameRespose | null = null;
|
||||
|
||||
while (!deviceNameResponse) {
|
||||
try {
|
||||
deviceNameResponse = await request({
|
||||
uri: `${process.env.BALENA_SUPERVISOR_ADDRESS}/v2/device/name?apikey=${process.env.BALENA_SUPERVISOR_API_KEY}`,
|
||||
json: true, method: 'GET'
|
||||
}).promise();
|
||||
} catch (_err) {
|
||||
console.log('Could not acquire device name from Supervisor, retrying in 10 seconds');
|
||||
await Bluebird.delay(10000);
|
||||
}
|
||||
}
|
||||
|
||||
return deviceNameResponse.deviceName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a new Avahi group for address publishing.
|
||||
*/
|
||||
const getGroup = async (): Promise<string> => {
|
||||
return await dbusInvoker({
|
||||
destination: 'org.freedesktop.Avahi',
|
||||
path: '/',
|
||||
interface: 'org.freedesktop.Avahi.Server',
|
||||
member: 'EntryGroupNew',
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a host address to the local domain.
|
||||
*
|
||||
* @param hostname Full hostname to publish
|
||||
* @param address IP address for the hostname
|
||||
*/
|
||||
const addHostAddress = async (
|
||||
hostname: string,
|
||||
address: string,
|
||||
): Promise<void> => {
|
||||
// If the hostname is already published with the same address, return
|
||||
if (_.find(publishedHosts, { hostname, address })) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Adding ${hostname} at address ${address} to local MDNS pool`);
|
||||
|
||||
// We require a new group for each address.
|
||||
// We don't catch errors, as our restart policy is to not restart.
|
||||
const group = await getGroup();
|
||||
|
||||
await dbusInvoker({
|
||||
destination: 'org.freedesktop.Avahi',
|
||||
path: group,
|
||||
interface: 'org.freedesktop.Avahi.EntryGroup',
|
||||
member: 'AddAddress',
|
||||
body: [-1, -1, 0x10, hostname, address],
|
||||
signature: 'iiuss',
|
||||
});
|
||||
|
||||
await dbusInvoker({
|
||||
destination: 'org.freedesktop.Avahi',
|
||||
path: group,
|
||||
interface: 'org.freedesktop.Avahi.EntryGroup',
|
||||
member: 'Commit',
|
||||
});
|
||||
|
||||
// Add to the published hosts list
|
||||
publishedHosts.push({
|
||||
group,
|
||||
hostname,
|
||||
address,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove hostname from published list
|
||||
*
|
||||
* @param hostname Hostname to remove from list
|
||||
* @param address IP address to remove from list
|
||||
*/
|
||||
const removeHostAddress = async (hostname: string, address: string): Promise<void> => {
|
||||
// If the hostname doesn't exist, we don't use it
|
||||
const hostDetails = _.find(publishedHosts, { hostname, address });
|
||||
if (!hostDetails) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Removing ${hostname} at address from local MDNS pool`);
|
||||
|
||||
// Free the group, removing the published address
|
||||
await dbusInvoker({
|
||||
destination: 'org.freedesktop.Avahi',
|
||||
path: hostDetails.group,
|
||||
interface: 'org.freedesktop.Avahi.EntryGroup',
|
||||
member: 'Free',
|
||||
});
|
||||
|
||||
// Remove from the published hosts list
|
||||
_.remove(publishedHosts, { hostname });
|
||||
};
|
||||
|
||||
|
||||
const publishNameAndAddress = async (): Promise<void> => {
|
||||
let address: string;
|
||||
// Get IP address for the specified interface.
|
||||
if (process.env.INTERFACE) {
|
||||
address = getNamedInterfaceAddr(process.env.INTERFACE);
|
||||
} else {
|
||||
address = await getDefaultInterfaceAddr();
|
||||
}
|
||||
|
||||
let deviceName: string = await getDeviceName();
|
||||
let hostname: string = `${deviceName}.local`;
|
||||
|
||||
if (!_.find(publishedHosts, { hostname, address })) {
|
||||
publishedHosts.forEach(async ({ hostname, address }) => await removeHostAddress(hostname, address));
|
||||
addHostAddress(hostname, address)
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
while (true) {
|
||||
await publishNameAndAddress();
|
||||
await Bluebird.delay(10000);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.log(`balena MDNS publisher error:\n${err}`);
|
||||
// This is not ideal. However, dbus-native does not correctly free connections
|
||||
// on event loop exit
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
22
mdns/tsconfig.json
Normal file
22
mdns/tsconfig.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"noImplicitAny": true,
|
||||
"removeComments": true,
|
||||
"preserveConstEnums": true,
|
||||
"strictNullChecks": true,
|
||||
"sourceMap": true,
|
||||
"target": "es6",
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"outDir": "build"
|
||||
},
|
||||
"include": [
|
||||
"typings/**/*.d.ts",
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
21
mdns/typings/dbus-native.d.ts
vendored
Normal file
21
mdns/typings/dbus-native.d.ts
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
declare module 'dbus-native' {
|
||||
export type BodyEntry = string | number | null;
|
||||
|
||||
export interface Message {
|
||||
path: string;
|
||||
destination: string;
|
||||
member: string;
|
||||
interface: string;
|
||||
body?: BodyEntry[];
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
export interface Bus {
|
||||
invoke: (
|
||||
message: Message,
|
||||
callback: (error: Error, response: any) => void,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export function systemBus(): Bus;
|
||||
}
|
10
ppp/Dockerfile.template
Normal file
10
ppp/Dockerfile.template
Normal file
|
@ -0,0 +1,10 @@
|
|||
FROM balenalib/%%BALENA_MACHINE_NAME%%-alpine
|
||||
|
||||
RUN install_packages ppp
|
||||
RUN mkdir -p /etc/ppp
|
||||
COPY options /etc/ppp/options
|
||||
|
||||
ENV SERIAL_PORT=/dev/ttyS0
|
||||
ENV BAUD_RATE=115200
|
||||
|
||||
CMD /usr/sbin/pppd $SERIAL_PORT $BAUD_RATE "$(printf "10.77.%d.%d:\n" "$((RANDOM % 256 ))" "$((RANDOM % 256 ))")"
|
8
ppp/options
Normal file
8
ppp/options
Normal file
|
@ -0,0 +1,8 @@
|
|||
lock
|
||||
noauth
|
||||
local
|
||||
nocrtscts
|
||||
xonxoff
|
||||
nobsdcomp
|
||||
nodeflate
|
||||
nodetach
|
Reference in a new issue