{ clanLib, ... }: { _class = "clan.service"; manifest.name = "phonebox"; manifest.description = ""; manifest.categories = [ "System" ]; roles.default = { interface = { lib, ... }: { options.ata-ethernet-iface = lib.mkOption { type = lib.types.str; description = "An Ethernet interface that connect to ATA box."; default = "enp2s0"; }; }; perInstance = { roles, settings, ... }: { nixosModule = { lib, config, ... }: let user = "asterisk"; rtpPortFrom = 10000; rtpPortTo = 20000; ata-interface = settings.ata-ethernet-iface; genServerSIPEndpoint = { hostname, address }: '' [${hostname}](internal_endpoint) aors=${hostname} [${hostname}](ip_auth) endpoint=${hostname} match=[${address}] [${hostname}](dynamiic_aor) contact=sip:[${address}] ''; genLocalSIPEndpoint = { localNumber }: '' [${localNumber}](internal_endpoint) aors=${localNumber} auth=${localNumber} [${localNumber}](userpass_auth) username=${localNumber} password=${localNumber} [${localNumber}](dynamiic_aor) max_contacts=1 ''; genLocalExtenConf = { localNumber }: '' exten => ${localNumber},1,Dial(PJSIP/${localNumber},20) ''; genExtentConf = { prefixNumber, hostname, localNumber, }: let replaceWithX = ln: builtins.concatStringsSep "" (builtins.genList (_: "X") (builtins.stringLength ln)); in '' exten => _${prefixNumber}${replaceWithX localNumber},1,Dial(PJSIP/''${EXTEN:1}@${hostname},30) ''; getYggdrasilIP = machineName: if config.clan.core.vars.generators.yggdrasil.files.address ? value then clanLib.getPublicValue { flake = config.clan.core.settings.directory; machine = machineName; generator = "yggdrasil"; file = "address"; default = null; } else throw "clanService/yggdrasil is required"; in { clan.core.vars.generators.phonebox = { files = { server-prefix-number.secret = false; ata-local-number.secret = false; }; prompts = { server-prefix-number = { type = "line"; description = "Server prefix number: indicate server to connect to [10XX]"; }; ata-local-number = { type = "line"; description = "Local suffix number: indicate local number on the server [XX00]"; }; }; script = '' cat $prompts/server-prefix-number > $out/server-prefix-number cat $prompts/ata-local-number > $out/ata-local-number ''; }; networking.interfaces = { ${ata-interface} = { useDHCP = false; ipv4.addresses = [ { address = "192.168.254.1"; prefixLength = 24; } ]; }; }; services.dnsmasq = { enable = true; settings = { bind-dynamic = true; listen-address = "192.168.254.1"; # enable-ra = true; domain-needed = true; domain = "localhost"; dhcp-range = [ "192.168.254.100,192.168.254.100,255.255.255.0,24h" ]; dhcp-option = [ "3,192.168.254.1" ]; interface = [ ata-interface ]; }; }; services.nginx = { enable = true; virtualHosts = { "_" = { locations."/" = { proxyPass = "http://192.168.254.100"; extraConfig = '' client_max_body_size 100M; ''; }; }; }; }; networking.firewall.allowedUDPPortRanges = [ { from = rtpPortFrom; to = rtpPortTo; } ]; networking.firewall.allowedUDPPorts = [ 53 67 5060 ]; networking.firewall.allowedTCPPorts = [ 53 ]; networking.firewall.interfaces = let matchAll = if !config.networking.nftables.enable then "zt+" else "zt*"; in { "${matchAll}".allowedTCPPorts = [ 80 ]; }; services.asterisk = { enable = lib.mkDefault true; confFiles = let machines = lib.attrNames roles.default.machines; nodes = builtins.foldl' ( nodes: name: nodes ++ [ { hostname = name; address = getYggdrasilIP name; prefixNumber = clanLib.getPublicValue { flake = config.clan.core.settings.directory; machine = name; generator = "phonebox"; file = "server-prefix-number"; default = null; }; localNumber = clanLib.getPublicValue { flake = config.clan.core.settings.directory; machine = name; generator = "phonebox"; file = "ata-local-number"; default = null; }; } ] ) [ ] machines; in { "logger.conf" = '' [general] dateformat = %F %T.%3q ; ISO 8601 date format with milliseconds use_callids = yes appendhostname = no queue_log = yes queue_log_to_file = no queue_log_name = queue_log queue_log_realtime_use_gmt = no rotatestrategy = rotate exec_after_rotate=gzip -9 $\{filename\}.2 [logfiles] console => notice,warning,error security => security messages => notice,warning,error full => notice,warning,error,verbose,dtmf,fax syslog.local0 => notice,warning,error ''; # Dial plan config "extensions.conf" = let serverConf = builtins.foldl' ( config: node: config + (genExtentConf { inherit (node) prefixNumber hostname localNumber; }) ) "" nodes; in '' [from-internal] exten => 999,1,Answer() same => n,Playback(hello-world) same => n,Hangup() '' + (genLocalExtenConf { localNumber = config.clan.core.vars.generators.phonebox.files.ata-local-number.value; }) + serverConf; "rtp.conf" = '' [general] rtpstart=${builtins.toString rtpPortFrom} rtpend=${builtins.toString rtpPortTo} ''; "pjsip.conf" = let serverConf = builtins.foldl' ( conf: node: conf + (genServerSIPEndpoint { hostname = node.hostname; address = node.address; }) ) "" nodes; in '' [transport-udp] type=transport protocol=udp bind=0.0.0.0 [transport-udp6] type=transport protocol=udp bind=:: [base_endpoint](!) type=endpoint disallow=all allow=ulaw,alaw,g722,gsm direct_media=no [internal_endpoint](!,base_endpoint) context=from-internal [userpass_auth](!) type=auth auth_type=userpass [ip_auth](!) type=identify endpoint=external [dynamiic_aor](!) type=aor '' + (genLocalSIPEndpoint { localNumber = config.clan.core.vars.generators.phonebox.files.ata-local-number.value; }) + serverConf; }; }; }; }; }; }