{
  "version": "v1",
  "variables": {
    "XTNA_TOKEN": {
      "type": "STRING",
      "label": "Asset creation token (XplicitTrust console)",
      "value": "",
      "system": false,
      "restricted": false
    },
    "XTNA_DOMAIN": {
      "type": "USER_TYPE",
      "label": "Tenant domain, e.g. example.com",
      "value": "",
      "system": false,
      "restricted": false,
      "userTypeName": "XTNA_Domain"
    },
    "XTNA_NAME": {
      "type": "STRING",
      "label": "Asset name; also sets -ignore-hostname",
      "value": "",
      "system": false,
      "restricted": false
    },
    "XTNA_IMAGE": {
      "type": "STRING",
      "label": "Container image",
      "value": "cr-public.xplicittrust.com/xtna-agent:latest",
      "system": false,
      "restricted": false
    },
    "XTNA_CONTAINER": {
      "type": "STRING",
      "label": "Container object name",
      "value": "xtna-agent",
      "system": false,
      "restricted": false
    },
    "XTNA_IP": {
      "type": "USER_TYPE",
      "label": "Fixed address in the docker network (blank = assigned)",
      "value": "",
      "system": false,
      "restricted": false,
      "userTypeName": "XTNA_IPv4"
    },
    "XTNA_NETWORK": {
      "type": "STRING",
      "label": "Docker network name",
      "value": "Docker",
      "system": false,
      "restricted": false
    },
    "XTNA_SUBNET": {
      "type": "USER_TYPE",
      "label": "Docker network subnet",
      "value": "10.238.238.0/24",
      "system": false,
      "restricted": false,
      "userTypeName": "XTNA_CIDR"
    },
    "XTNA_GATEWAY": {
      "type": "USER_TYPE",
      "label": "Docker network gateway (only when creating it)",
      "value": "10.238.238.254",
      "system": false,
      "restricted": false,
      "userTypeName": "XTNA_IPv4"
    },
    "XTNA_CREATE_NETWORK": {
      "type": "USER_TYPE",
      "label": "Declare the docker network too (off if it already exists)",
      "value": "off",
      "system": false,
      "restricted": false,
      "userTypeName": "XTNA_OnOff"
    },
    "XTNA_WAN_ACCESS": {
      "type": "USER_TYPE",
      "label": "Create the desktop object and WAN connection",
      "value": "on",
      "system": false,
      "restricted": false,
      "userTypeName": "XTNA_OnOff"
    },
    "XTNA_BRIDGE": {
      "type": "STRING",
      "label": "Docker bridge interface name, e.g. dockereaecd76f",
      "value": "",
      "system": false,
      "restricted": false
    },
    "XTNA_SERVICES": {
      "type": "STRING",
      "label": "Outbound services (names or uuids), comma separated",
      "value": "internet.https,standard.dns",
      "system": false,
      "restricted": false
    },
    "XTNA_INTERNET": {
      "type": "STRING",
      "label": "Internet object name (only if more than one)",
      "value": "",
      "system": false,
      "restricted": false
    },
    "XTNA_DESKTOP": {
      "type": "STRING",
      "label": "Desktop object name",
      "value": "XTNA container",
      "system": false,
      "restricted": false
    }
  },
  "applications": {
    "XTNA-Agent": {
      "script": {
        "lcos": false,
        "swos": false,
        "lcosFx": true,
        "lcosLx": false,
        "lcosSxXs": false,
        "lcosSxSdk4": false,
        "content": "/**\n * XplicitTrust Network Access - agent container deployment\n * LMC add-in for LANCOM R&S Unified Firewalls (LCOS FX 11.2+, SAG Basic/Full licence)\n *\n * ---------------------------------------------------------------------------\n * How the LCOS FX add-in API actually works\n * ---------------------------------------------------------------------------\n * Probed on a live UF: ufApi.raw() returns undefined for every URI, so there is\n * NO way to read device state from an add-in. ufApi.lookup() does not return a\n * value either - it returns a deferred reference whose string form is a JSONPath\n * expression resolved when the configuration is applied:\n *\n *     String(ufApi.lookup(\"internet\", { name: \"WAN\" }))\n *       -> \"\\x02/model/internet\\x05$[?(@.name==\\\"WAN\\\")].uniqueId\\x03\"\n *\n * So an add-in is write-only and declarative. It cannot check whether an object\n * exists, cannot diff, and cannot branch on anything the firewall holds. It\n * states the objects that should exist and lets the rollout reconcile them,\n * which is why LANCOM's own examples call createObject unconditionally and pass\n * lookup() straight into the payload.\n *\n * Everything below follows that model. The only branches are on add-in\n * variables, which are known at script time.\n *\n * ---------------------------------------------------------------------------\n * Add-in variables  (LMC: Add-ins > Variables)\n * ---------------------------------------------------------------------------\n *   XTNA_TOKEN       required  Asset creation token from the XplicitTrust console\n *   XTNA_DOMAIN      required  Tenant domain, e.g. example.com\n *   XTNA_NAME        optional  Asset name. When set, -ignore-hostname is added too.\n *\n *   XTNA_IMAGE       optional  default cr-public.xplicittrust.com/xtna-agent:latest\n *   XTNA_CONTAINER   optional  default xtna-agent          (container object name)\n *   XTNA_IP          optional  fixed address inside the docker network. Leave\n *                              blank to let Docker's IPAM assign one.\n *   XTNA_NETWORK     optional  default Docker              (docker network name)\n *\n *   XTNA_CREATE_NETWORK  on | off (default off)\n *                    Declare the docker network too. Leave off when the network\n *                    already exists on the firewall - the add-in cannot check,\n *                    and declaring an existing network risks a duplicate.\n *   XTNA_SUBNET      the docker network's subnet. Used for the desktop object,\n *                    and to create the network when XTNA_CREATE_NETWORK=on.\n *   XTNA_GATEWAY     only used when XTNA_CREATE_NETWORK=on\n *\n *   XTNA_WAN_ACCESS  on | off (default on) - declare the desktop object and the\n *                    connection to the Internet object\n *   XTNA_BRIDGE      required for WAN access. Name of the docker bridge in\n *                    /model/interfaces, e.g. dockereaecd76f. LCOS FX names it\n *                    \"docker\" + the first 8 hex of the docker network's uniqueId.\n *                    It cannot be derived at script time because the uniqueId is\n *                    not readable, so it has to be given explicitly.\n *   XTNA_INTERNET    optional  default WAN\n *   XTNA_SERVICES    optional  default \"internet.https,standard.dns\". Predefined\n *                              service names are category-prefixed. A uniqueId may\n *                              be given instead of a name.\n *   XTNA_DESKTOP     optional  default \"XTNA container\"\n *\n * If neither XTNA_TOKEN nor XTNA_DOMAIN is set, the add-in does nothing on that\n * device, so it is safe to allocate broadly.\n *\n * ---------------------------------------------------------------------------\n * Notes\n * ---------------------------------------------------------------------------\n * - The volume at /etc/XplicitTrust is what makes the asset identity survive a\n *   restart. Without it every recreate enrols a brand new asset. The launcher\n *   only registers when /etc/XplicitTrust/config/selected_config is absent, so\n *   re-declaring -token on an enrolled container is harmless.\n * - XTNA_TOKEN ends up in the container command line, which is stored in the\n *   firewall configuration and included in configuration exports. Use a token\n *   scoped to asset creation and disable it once the rollout is done.\n * - With only https+dns outbound the agent works but every peer connection falls\n *   back to the relay. There is no predefined service covering the WireGuard and\n *   peering UDP ports, so direct peering needs a userdefined service.\n *\n * @param {Config} config\n * @param {Context} context\n * Do not edit this comment or parameter types. Required for code suggestions\n */\nexports.main = function (config, context) {\n\n    var DEFAULTS = {\n        image:     \"cr-public.xplicittrust.com/xtna-agent:latest\",\n        container: \"xtna-agent\",\n        network:   \"Docker\",\n        subnet:    \"10.238.238.0/24\",\n        gateway:   \"10.238.238.254\",\n        desktop:   \"XTNA container\",\n        internet:  \"WAN\",\n        services:  \"internet.https,standard.dns\"\n    };\n\n    var vars = (context && context.vars) || {};\n\n    function str(v) {\n        if (v === null || v === undefined) { return \"\"; }\n        return String(v).replace(/^\\s+|\\s+$/g, \"\");\n    }\n\n    function isOn(v, fallback) {\n        var text = str(v);\n        if (!text) { return fallback; }\n        return /^(on|true|yes|1)$/i.test(text);\n    }\n\n    function log(message) {\n        config.infoLog(\"xtna-agent: \" + message);\n    }\n\n    var token  = str(vars.XTNA_TOKEN);\n    var domain = str(vars.XTNA_DOMAIN);\n    var name   = str(vars.XTNA_NAME);\n\n    if (!token && !domain) {\n        log(\"XTNA_TOKEN/XTNA_DOMAIN not assigned on this device - skipping.\");\n        return;\n    }\n    if (!token || !domain) {\n        config.abort(\"xtna-agent: XTNA_TOKEN and XTNA_DOMAIN must both be set (got token=\" +\n                     (token ? \"yes\" : \"no\") + \", domain=\" + (domain ? \"yes\" : \"no\") + \").\");\n        return;\n    }\n    if (/\\s/.test(token) || /\\s/.test(domain) || /\\s/.test(name)) {\n        config.abort(\"xtna-agent: XTNA_TOKEN, XTNA_DOMAIN and XTNA_NAME must not contain whitespace - \" +\n                     \"the container command line is split on spaces.\");\n        return;\n    }\n\n    var image        = str(vars.XTNA_IMAGE)     || DEFAULTS.image;\n    var ctName       = str(vars.XTNA_CONTAINER) || DEFAULTS.container;\n    var netName      = str(vars.XTNA_NETWORK)   || DEFAULTS.network;\n    var deskName     = str(vars.XTNA_DESKTOP)   || DEFAULTS.desktop;\n    var internetName = str(vars.XTNA_INTERNET)  || DEFAULTS.internet;\n    var bridgeName   = str(vars.XTNA_BRIDGE);\n    var subnet       = str(vars.XTNA_SUBNET)    || DEFAULTS.subnet;\n\n    var createNetwork = isOn(vars.XTNA_CREATE_NETWORK, false);\n    var wantWan       = isOn(vars.XTNA_WAN_ACCESS, true);\n\n    var argv = [\"-token\", token, \"-domain\", domain];\n    if (name) {\n        argv.push(\"-name\", name, \"-ignore-hostname\");\n    }\n    var command = argv.join(\" \");\n\n    if (command.length > 1024) {\n        config.abort(\"xtna-agent: container command line is \" + command.length +\n                     \" characters, the firewall accepts at most 1024.\");\n        return;\n    }\n\n    var ufApi = config.getUfApi();\n\n    // A reference resolved when the configuration is applied, not now.\n    function ref(entity, criteria) {\n        return String(ufApi.lookup(entity, criteria));\n    }\n\n    /* ------------------------------------------------------------------ *\n     * 1. docker network - only when asked for\n     * ------------------------------------------------------------------ */\n\n    if (createNetwork) {\n        var gateway = str(vars.XTNA_GATEWAY) || DEFAULTS.gateway;\n        ufApi.createObject(\"docker-network-settings\", {\n            name:    netName,\n            subnet:  subnet,\n            gateway: gateway\n        });\n        log(\"declared docker network '\" + netName + \"' (\" + subnet + \", gateway \" + gateway + \")\");\n    } else {\n        log(\"using existing docker network '\" + netName + \"' (XTNA_CREATE_NETWORK is off)\");\n    }\n\n    /* ------------------------------------------------------------------ *\n     * 2. agent container\n     * ------------------------------------------------------------------ */\n\n    var attachment = { uniqueId: ref(\"docker-network-settings\", { name: netName }) };\n    var fixedIp = str(vars.XTNA_IP);\n    if (fixedIp) {\n        attachment.ip = fixedIp;\n    }\n\n    ufApi.createObject(\"docker-container-settings\", {\n        active:       true,\n        name:         ctName,\n        image:        image,\n        command:      command,\n        capabilities: [\"NET_ADMIN\", \"NET_RAW\"],\n        volumes:      [\"/etc/XplicitTrust\"],\n        networks:     [attachment],\n        policy:       { restart: { condition: \"always\" } }\n    });\n    log(\"declared container '\" + ctName + \"' from \" + image +\n        (name ? \" as asset '\" + name + \"'\" : \" (asset name from the container id)\"));\n\n    /* ------------------------------------------------------------------ *\n     * 3. outbound access for the container network\n     * ------------------------------------------------------------------ */\n\n    if (!wantWan) {\n        log(\"XTNA_WAN_ACCESS=off - not declaring desktop objects.\");\n        return;\n    }\n\n    // Not an abort: aborting discards the whole config build for the device,\n    // including the container declared above. Skipping leaves the container\n    // deployed and only the outbound rule missing, which is recoverable.\n    if (!bridgeName) {\n        config.warnLog(\"xtna-agent: XTNA_WAN_ACCESS is on but XTNA_BRIDGE is empty, so the container \" +\n                       \"has no outbound rule and cannot enrol. Set XTNA_BRIDGE to the docker bridge \" +\n                       \"name from /model/interfaces (type \\\"bridge\\\", named docker<8 hex>) and roll \" +\n                       \"out again. It cannot be derived because add-ins cannot read device state.\");\n        return;\n    }\n\n    ufApi.createObject(\"networks\", {\n        name:      deskName,\n        interface: ref(\"interfaces\", { name: bridgeName }),\n        ipv4:      subnet,\n        icon:      \"network\"\n    });\n    log(\"declared desktop object '\" + deskName + \"' on bridge '\" + bridgeName + \"' (\" + subnet + \")\");\n\n    var wantedServices = (str(vars.XTNA_SERVICES) || DEFAULTS.services).split(\",\");\n    var rules = [];\n    var named = [];\n\n    // An entry may be a service name or a literal uniqueId. Names are resolved by\n    // reference at apply time and silently become null if nothing matches, which\n    // fails validation with err.general.required - so a uuid is the escape hatch\n    // when a name does not match what the firewall calls the service.\n    var UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;\n\n    for (var i = 0; i < wantedServices.length; i++) {\n        var svcName = str(wantedServices[i]);\n        if (!svcName) { continue; }\n        rules.push({\n            uniqueId:  UUID.test(svcName) ? svcName : ref(\"predefined-services\", { name: svcName }),\n            action:    \"leftToRight\",\n            natactive: \"left\"\n        });\n        named.push(svcName);\n    }\n\n    if (!rules.length) {\n        config.abort(\"xtna-agent: XTNA_SERVICES is empty, the connection would allow nothing.\");\n        return;\n    }\n\n    // natactive \"left\" masquerades the container side. The docker subnet is not\n    // routable upstream, so without it packets leave with a 10.x source and\n    // nothing comes back.\n    ufApi.createObject(\"desktop-connections\", {\n        obja:           ref(\"networks\", { name: deskName }),\n        objb:           ref(\"internet\", { name: internetName }),\n        natactive:      \"left\",\n        trafficShaping: {},\n        rules:          rules\n    });\n    log(\"declared connection '\" + deskName + \"' <-> '\" + internetName + \"' allowing \" +\n        named.join(\", \") + \" outbound\");\n};\n"
      },
      "application": {
        "type": "SCRIPT",
        "enabled": true,
        "resetToDefault": false,
        "usedForOneTimeExecution": false
      },
      "applicationVariables": {
        "XTNA_TOKEN": {
          "restricted": false
        },
        "XTNA_DOMAIN": {
          "restricted": false
        },
        "XTNA_NAME": {
          "restricted": false
        },
        "XTNA_IMAGE": {
          "restricted": false
        },
        "XTNA_CONTAINER": {
          "restricted": false
        },
        "XTNA_IP": {
          "restricted": false
        },
        "XTNA_NETWORK": {
          "restricted": false
        },
        "XTNA_SUBNET": {
          "restricted": false
        },
        "XTNA_GATEWAY": {
          "restricted": false
        },
        "XTNA_CREATE_NETWORK": {
          "restricted": false
        },
        "XTNA_WAN_ACCESS": {
          "restricted": false
        },
        "XTNA_BRIDGE": {
          "restricted": false
        },
        "XTNA_SERVICES": {
          "restricted": false
        },
        "XTNA_INTERNET": {
          "restricted": false
        },
        "XTNA_DESKTOP": {
          "restricted": false
        }
      }
    }
  },
  "variableTypes": {
    "XTNA_OnOff": {
      "type": "OPTIONS",
      "options": [
        {
          "value": "on",
          "displayName": "on"
        },
        {
          "value": "off",
          "displayName": "off"
        }
      ]
    },
    "XTNA_Domain": {
      "type": "REGEX",
      "regex": "^$|^(?=.{1,253}$)([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,63}$"
    },
    "XTNA_IPv4": {
      "type": "REGEX",
      "regex": "^$|^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"
    },
    "XTNA_CIDR": {
      "type": "REGEX",
      "regex": "^$|^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.([01]?\\d\\d?|2[0-4]\\d|25[0-5])/([1-9]|[12]\\d|3[01])$"
    }
  }
}
