244 lines
8.9 KiB
Nix
244 lines
8.9 KiB
Nix
{ lib, pkgs, config, ... }:
|
|
|
|
let
|
|
cfg = config.services.voip;
|
|
|
|
# Per-model config. Adding a new hardware model:
|
|
# 1. Add an entry here with all required fields
|
|
# 2. For provisioned models, add a template in ./templates/<model>.nix
|
|
#
|
|
# Template interface — all provisioned model templates receive these args:
|
|
# Required: mac, label, password, displayName, serverAddress, ntpServer
|
|
# Optional: sipPort (default 5060), directoryPort (default 8080),
|
|
# intercomEnabled (default false), intercomPassword (default "")
|
|
models = {
|
|
"cisco-8961" = {
|
|
endpointTemplate = "endpoint-cisco-8961";
|
|
maxContacts = 1;
|
|
hasProvisioning = true;
|
|
desktopSize = "640x480x24";
|
|
thumbnailSize = "123x111";
|
|
template = import ./templates/cisco-8961.nix;
|
|
};
|
|
"sip-client" = {
|
|
endpointTemplate = "endpoint-generic";
|
|
maxContacts = 1;
|
|
hasProvisioning = false;
|
|
desktopSize = null;
|
|
thumbnailSize = null;
|
|
template = null;
|
|
};
|
|
};
|
|
|
|
intercomEntries = import ./intercom.nix { inherit lib cfg models; };
|
|
confFiles = import ./asterisk.nix { inherit lib cfg models intercomEntries; };
|
|
directory = import ./directory.nix { inherit lib pkgs cfg intercomEntries; };
|
|
backgroundEntries = import ./backgrounds.nix { inherit lib pkgs cfg models; };
|
|
tftpRoot = import ./tftp.nix { inherit lib pkgs cfg models backgroundEntries; };
|
|
|
|
in {
|
|
options.services.voip = {
|
|
enable = lib.mkEnableOption "VoIP provisioning (Asterisk + TFTP)";
|
|
|
|
serverAddress = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "IP address or hostname of this server (used in phone configs and SIP).";
|
|
};
|
|
|
|
ntpServer = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "NTP server for phones. Defaults to serverAddress.";
|
|
default = "";
|
|
};
|
|
|
|
sipPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 5060;
|
|
};
|
|
|
|
rtpStart = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 10000;
|
|
};
|
|
|
|
rtpEnd = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 20000;
|
|
};
|
|
|
|
directoryName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "tel.baubs.net";
|
|
description = "Name shown in the phone directory title.";
|
|
};
|
|
|
|
directoryPort = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 8080;
|
|
description = "HTTP port for the phone directory and services.";
|
|
};
|
|
|
|
backgroundImages = lib.mkOption {
|
|
default = {};
|
|
description = ''
|
|
Attrset of background images keyed by display name.
|
|
Value is a path to any image file — it will be resized automatically
|
|
to the correct dimensions for each phone model during build.
|
|
For best results, use a 4:3 aspect ratio source image.
|
|
'';
|
|
type = lib.types.attrsOf lib.types.path;
|
|
};
|
|
|
|
phones = lib.mkOption {
|
|
default = {};
|
|
description = ''
|
|
Attrset of phones/clients keyed by SIP identity (username).
|
|
For hardware phones (cisco-8961), the key must be the lowercase MAC address (no colons).
|
|
For sip-client, the key is a free-form username.
|
|
'';
|
|
type = lib.types.attrsOf (lib.types.submodule {
|
|
options = {
|
|
model = lib.mkOption {
|
|
type = lib.types.enum (lib.attrNames models);
|
|
description = "Phone model. Use \"sip-client\" for software SIP clients (no provisioning file).";
|
|
};
|
|
extension = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Extension number this phone registers as.";
|
|
};
|
|
label = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = "Label shown on the phone screen. Required for provisioned hardware phones.";
|
|
};
|
|
password = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "SIP registration password.";
|
|
};
|
|
voicemailTimeout = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.ints.positive;
|
|
default = null;
|
|
description = "Seconds to ring before sending to voicemail. null disables voicemail for this phone.";
|
|
};
|
|
};
|
|
});
|
|
};
|
|
|
|
intercomPrefix = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Dial prefix for auto-generated intercom extensions. e.g. \"*80\" generates *80100 for ext 100. Only intercom-capable (provisioned) phones get entries.";
|
|
};
|
|
|
|
extensions = lib.mkOption {
|
|
default = {};
|
|
description = "Attrset of extensions keyed by extension number.";
|
|
type = lib.types.attrsOf (lib.types.submodule {
|
|
options = {
|
|
mode = lib.mkOption {
|
|
type = lib.types.enum [ "line" "page" "app" ];
|
|
default = "line";
|
|
description = ''
|
|
Extension mode:
|
|
- "line": dials the phone assigned to this extension
|
|
- "page": one-way announcement to all phones
|
|
- "app": custom Asterisk dialplan application
|
|
Intercom extensions are auto-generated when intercomPrefix is set.
|
|
'';
|
|
};
|
|
displayName = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
};
|
|
app = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Verbatim Asterisk dialplan app. Required for mode = \"app\".";
|
|
};
|
|
};
|
|
});
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
|
|
services.voip.ntpServer = lib.mkDefault cfg.serverAddress;
|
|
|
|
|
|
assertions =
|
|
# Every phone's extension must be declared
|
|
(lib.mapAttrsToList (key: phone: {
|
|
assertion = lib.hasAttr phone.extension cfg.extensions;
|
|
message = "services.voip: phone \"${key}\" references extension ${phone.extension} which is not declared in services.voip.extensions";
|
|
}) cfg.phones)
|
|
++
|
|
# Provisioned phones require a MAC address key
|
|
(lib.mapAttrsToList (key: phone: {
|
|
assertion = !models.${phone.model}.hasProvisioning || builtins.match "[0-9a-f]{12}" key != null;
|
|
message = "services.voip: phone \"${key}\" (model ${phone.model}) requires a lowercase 12-char hex MAC address as key";
|
|
}) cfg.phones)
|
|
++
|
|
# Provisioned phones require a non-empty label
|
|
(lib.mapAttrsToList (key: phone: {
|
|
assertion = !models.${phone.model}.hasProvisioning || phone.label != "";
|
|
message = "services.voip: phone \"${key}\" (model ${phone.model}) requires a non-empty label";
|
|
}) cfg.phones)
|
|
++
|
|
# Provisioned phones require a template
|
|
(lib.mapAttrsToList (key: phone: {
|
|
assertion = !models.${phone.model}.hasProvisioning || models.${phone.model}.template != null;
|
|
message = "services.voip: phone \"${key}\" model \"${phone.model}\" has hasProvisioning=true but no template defined";
|
|
}) cfg.phones)
|
|
++
|
|
# intercomPrefix must not collide with user-declared extensions
|
|
(lib.optionals (cfg.intercomPrefix != null)
|
|
(lib.mapAttrsToList (key: phone:
|
|
let ext = "${cfg.intercomPrefix}${phone.extension}"; in {
|
|
assertion = !lib.hasAttr ext cfg.extensions;
|
|
message = "services.voip: auto-generated intercom extension \"${ext}\" collides with a declared extension; rename it or change intercomPrefix";
|
|
}
|
|
) (lib.filterAttrs (_: phone: models.${phone.model}.hasProvisioning) cfg.phones)));
|
|
|
|
services.asterisk = {
|
|
enable = true;
|
|
confFiles = confFiles;
|
|
};
|
|
|
|
services.atftpd = {
|
|
enable = true;
|
|
root = "${tftpRoot}";
|
|
extraOptions = [ "--verbose=7" ];
|
|
};
|
|
|
|
services.nginx = {
|
|
enable = true;
|
|
virtualHosts."voip-directory" = {
|
|
listen = [{ addr = "0.0.0.0"; port = cfg.directoryPort; }];
|
|
locations."= /directory.xml" = {
|
|
alias = "${directory.menuFile}";
|
|
extraConfig = "default_type text/xml;";
|
|
};
|
|
locations."= /directory-list.xml" = {
|
|
alias = "${directory.listFile}";
|
|
extraConfig = "default_type text/xml;";
|
|
};
|
|
locations."= /intercom.xml" = {
|
|
alias = "${directory.intercomFile}";
|
|
extraConfig = "default_type text/xml;";
|
|
};
|
|
locations."= /voicemail.xml" = {
|
|
alias = "${directory.voicemailFile}";
|
|
extraConfig = "default_type text/xml;";
|
|
};
|
|
};
|
|
};
|
|
|
|
systemd.tmpfiles.rules = [ "d /var/log/nginx 0750 nginx nginx -" ];
|
|
|
|
networking.firewall = {
|
|
allowedTCPPorts = [ cfg.sipPort cfg.directoryPort ];
|
|
allowedUDPPorts = [ 69 ];
|
|
allowedUDPPortRanges = [{ from = cfg.rtpStart; to = cfg.rtpEnd; }];
|
|
};
|
|
};
|
|
}
|