From 3ff0a8d0fd4210d7722cac76c428a7cfe7e1b5da Mon Sep 17 00:00:00 2001 From: John Crispin Date: Wed, 25 May 2022 18:34:57 +0200 Subject: [PATCH] add missing radsec features Signed-off-by: John Crispin --- renderer/templates/services/radius_proxy.uc | 47 +- schema/service.radius-proxy.yml | 191 ++++++-- schemareader.uc | 499 ++++++++++++++++---- ucentral.schema.json | 177 +++++-- 4 files changed, 719 insertions(+), 195 deletions(-) diff --git a/renderer/templates/services/radius_proxy.uc b/renderer/templates/services/radius_proxy.uc index 3c0decf..c427aa7 100644 --- a/renderer/templates/services/radius_proxy.uc +++ b/renderer/templates/services/radius_proxy.uc @@ -23,12 +23,13 @@ set radsecproxy.@client[-1].secret='secret' certs.ca = files.add_anonymous(location, 'ca' + idx, b64dec(realm.ca_certificate)); certs.cert = files.add_anonymous(location, 'cert' + idx, b64dec(realm.certificate)); certs.key = files.add_anonymous(location, 'key' + idx, b64dec(realm.private_key)); - } else { + } else if (realm.protocol == "radsec") { warn("invalid certificate settings"); continue; } %} +{% if (realm.protocol == "radsec"): %} set radsecproxy.tls{{ idx }}=tls set radsecproxy.@tls[-1].name='tls{{ idx }}' set radsecproxy.@tls[-1].CACertificateFile={{ s(certs.ca) }} @@ -38,21 +39,57 @@ set radsecproxy.@tls[-1].certificateKeyPassword='' set radsecproxy.server{{ idx }}=server set radsecproxy.@server[-1].name='server{{ idx }}' -{% if (realm.auto_discover): %} +{% if (realm.auto_discover): %} set radsecproxy.@server[-1].dynamicLookupCommand='/usr/libexec/naptr_lookup.sh' -{% else %} +{% else %} set radsecproxy.@server[-1].host={{ s(realm.host) }} set radsecproxy.@server[-1].port={{ s(realm.port) }} set radsecproxy.@server[-1].secret={{ s(realm.secret) }} -{% endif %} +{% endif %} set radsecproxy.@server[-1].type='tls' set radsecproxy.@server[-1].tls='tls{{ idx }}' set radsecproxy.@server[-1].statusServer='0' set radsecproxy.@server[-1].certificateNameCheck='0' +{% for (name in realm.realm): %} add radsecproxy realm -set radsecproxy.@realm[-1].name='{{ realm.realm }}' +set radsecproxy.@realm[-1].name='{{ name }}' set radsecproxy.@realm[-1].server='server{{ idx }}' set radsecproxy.@realm[-1].accountingServer='server{{ idx }}' +{% endfor %} +{% else if (realm.protocol == "radius"): %} + +set radsecproxy.server{{ idx + "auth" }}=server +set radsecproxy.@server[-1].name='server{{ idx }}auth' +set radsecproxy.@server[-1].host={{ s(realm.auth_server) }} +set radsecproxy.@server[-1].port={{ s(realm.auth_port) }} +set radsecproxy.@server[-1].secret={{ s(realm.auth_secret) }} +set radsecproxy.@server[-1].type='udp' + +{% if (realm.acct_server): %} +set radsecproxy.server{{ idx + "acct" }}=server +set radsecproxy.@server[-1].name='server{{ idx }}acct' +set radsecproxy.@server[-1].host={{ s(realm.acct_server) }} +set radsecproxy.@server[-1].port={{ s(realm.acct_port) }} +set radsecproxy.@server[-1].secret={{ s(realm.acct_secret) }} +set radsecproxy.@server[-1].type='udp' +{% endif %} + +{% for (name in realm.realm): %} +add radsecproxy realm +set radsecproxy.@realm[-1].name='{{ name }}' +set radsecproxy.@realm[-1].server='server{{ idx }}auth' +{% if (realm.acct_server): %} +set radsecproxy.@realm[-1].accountingServer='server{{ idx }}' +{% endif %} +{% endfor %} + +{% else %} +{% for (name in realm.realm): %} +add radsecproxy realm +set radsecproxy.@realm[-1].name='{{ name }}' +set radsecproxy.@realm[-1].replyMessage=s(realm.message) +{% endfor %} +{% endif %} {% endfor %} diff --git a/schema/service.radius-proxy.yml b/schema/service.radius-proxy.yml index ce6be0b..f84a445 100644 --- a/schema/service.radius-proxy.yml +++ b/schema/service.radius-proxy.yml @@ -7,54 +7,143 @@ properties: The various realms that we can proxy to. type: array items: - type: object - properties: - realm: - description: - The realm that that this server shall be used for. - type: string - default: '*' - auto-discover: - description: - Auto discover radsec server address via realm DNS NAPTR record. - type: boolean - default: false - host: - description: - The remote proxy server that the device shall connect to. - type: string - format: uc-host - examples: - - 192.168.1.10 - port: - description: - The remote proxy port that the device shall connect to. - type: integer - maximum: 65535 - default: 2083 - secret: - description: - The radius secret that will be used for the connection. - type: string - use-local-certificates: - description: - The device will use its local certificate bundle for the TLS setup and - ignores all other certificate options in this section. - type: boolean - default: false - ca-certificate: - description: - The local servers CA bundle. - type: string - certificate: - description: - The local servers certificate. - type: string - private-key: - description: - The local servers private key/ - type: string - private-key-password: - description: - The password required to read the private key. - type: string + anyOf: + - type: object + properties: + protocol: + description: + Defines whether the real should use radsec or normal radius. + type: string + enum: + - radsec + default: radsec + realm: + description: + The realm that that this server shall be used for. + type: array + items: + type: string + default: '*' + auto-discover: + description: + Auto discover radsec server address via realm DNS NAPTR record. + type: boolean + default: false + host: + description: + The remote proxy server that the device shall connect to. + type: string + format: uc-host + examples: + - 192.168.1.10 + port: + description: + The remote proxy port that the device shall connect to. + type: integer + maximum: 65535 + default: 2083 + secret: + description: + The radius secret that will be used for the connection. + type: string + use-local-certificates: + description: + The device will use its local certificate bundle for the TLS setup and + ignores all other certificate options in this section. + type: boolean + default: false + ca-certificate: + description: + The local servers CA bundle. + type: string + certificate: + description: + The local servers certificate. + type: string + private-key: + description: + The local servers private key/ + type: string + private-key-password: + description: + The password required to read the private key. + type: string + - type: object + properties: + protocol: + description: + Defines whether the real should use radsec or normal radius. + type: string + enum: + - radius + realm: + description: + The realm that that this server shall be used for. + type: array + items: + type: string + default: '*' + auth-server: + description: + The URI of our Radius server. + type: string + format: uc-host + examples: + - 192.168.1.10 + auth-port: + description: + The network port of our Radius server. + type: integer + maximum: 65535 + minimum: 1024 + examples: + - 1812 + auth-secret: + description: + The shared Radius authentication secret. + type: string + examples: + - secret + acct-server: + description: + The URI of our Radius server. + type: string + format: uc-host + examples: + - 192.168.1.10 + acct-port: + description: + The network port of our Radius server. + type: integer + maximum: 65535 + minimum: 1024 + examples: + - 1812 + acct-secret: + description: + The shared Radius authentication secret. + type: string + examples: + - secret + - type: object + properties: + protocol: + description: + Defines whether the real should use radsec or normal radius. + type: string + enum: + - block + realm: + description: + The realm that that this server shall be used for. + type: array + items: + type: string + default: '*' + message: + description: + The message that is sent when a realm is blocked. + type: string + items: + type: string + default: 'blocked' diff --git a/schemareader.uc b/schemareader.uc index 683f8a0..32db302 100644 --- a/schemareader.uc +++ b/schemareader.uc @@ -5419,148 +5419,447 @@ function instantiateServiceRadiusProxy(location, value, errors) { function parseRealms(location, value, errors) { if (type(value) == "array") { function parseItem(location, value, errors) { - if (type(value) == "object") { - let obj = {}; + function parseVariant0(location, value, errors) { + if (type(value) == "object") { + let obj = {}; - function parseRealm(location, value, errors) { - if (type(value) != "string") - push(errors, [ location, "must be of type string" ]); + function parseProtocol(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); - return value; - } + if (!(value in [ "radsec" ])) + push(errors, [ location, "must be one of \"radsec\"" ]); - if (exists(value, "realm")) { - obj.realm = parseRealm(location + "/realm", value["realm"], errors); - } - else { - obj.realm = "*"; - } + return value; + } - function parseAutoDiscover(location, value, errors) { - if (type(value) != "bool") - push(errors, [ location, "must be of type boolean" ]); + if (exists(value, "protocol")) { + obj.protocol = parseProtocol(location + "/protocol", value["protocol"], errors); + } + else { + obj.protocol = "radsec"; + } - return value; - } + function parseRealm(location, value, errors) { + if (type(value) == "array") { + function parseItem(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); - if (exists(value, "auto-discover")) { - obj.auto_discover = parseAutoDiscover(location + "/auto-discover", value["auto-discover"], errors); - } - else { - obj.auto_discover = false; - } + return value; + } - function parseHost(location, value, errors) { - if (type(value) == "string") { - if (!matchUcHost(value)) - push(errors, [ location, "must be a valid hostname or IP address" ]); + return map(value, (item, i) => parseItem(location + "/" + i, item, errors)); + } + + if (type(value) != "array") + push(errors, [ location, "must be of type array" ]); + return value; } - if (type(value) != "string") - push(errors, [ location, "must be of type string" ]); + if (exists(value, "realm")) { + obj.realm = parseRealm(location + "/realm", value["realm"], errors); + } - return value; - } + function parseAutoDiscover(location, value, errors) { + if (type(value) != "bool") + push(errors, [ location, "must be of type boolean" ]); - if (exists(value, "host")) { - obj.host = parseHost(location + "/host", value["host"], errors); - } + return value; + } - function parsePort(location, value, errors) { - if (type(value) in [ "int", "double" ]) { - if (value > 65535) - push(errors, [ location, "must be lower than or equal to 65535" ]); + if (exists(value, "auto-discover")) { + obj.auto_discover = parseAutoDiscover(location + "/auto-discover", value["auto-discover"], errors); + } + else { + obj.auto_discover = false; + } + + function parseHost(location, value, errors) { + if (type(value) == "string") { + if (!matchUcHost(value)) + push(errors, [ location, "must be a valid hostname or IP address" ]); + + } + + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + return value; } - if (type(value) != "int") - push(errors, [ location, "must be of type integer" ]); + if (exists(value, "host")) { + obj.host = parseHost(location + "/host", value["host"], errors); + } - return value; - } + function parsePort(location, value, errors) { + if (type(value) in [ "int", "double" ]) { + if (value > 65535) + push(errors, [ location, "must be lower than or equal to 65535" ]); - if (exists(value, "port")) { - obj.port = parsePort(location + "/port", value["port"], errors); - } - else { - obj.port = 2083; - } + } - function parseSecret(location, value, errors) { - if (type(value) != "string") - push(errors, [ location, "must be of type string" ]); + if (type(value) != "int") + push(errors, [ location, "must be of type integer" ]); - return value; - } + return value; + } - if (exists(value, "secret")) { - obj.secret = parseSecret(location + "/secret", value["secret"], errors); - } + if (exists(value, "port")) { + obj.port = parsePort(location + "/port", value["port"], errors); + } + else { + obj.port = 2083; + } + + function parseSecret(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); - function parseUseLocalCertificates(location, value, errors) { - if (type(value) != "bool") - push(errors, [ location, "must be of type boolean" ]); + return value; + } - return value; - } + if (exists(value, "secret")) { + obj.secret = parseSecret(location + "/secret", value["secret"], errors); + } - if (exists(value, "use-local-certificates")) { - obj.use_local_certificates = parseUseLocalCertificates(location + "/use-local-certificates", value["use-local-certificates"], errors); - } - else { - obj.use_local_certificates = false; - } + function parseUseLocalCertificates(location, value, errors) { + if (type(value) != "bool") + push(errors, [ location, "must be of type boolean" ]); - function parseCaCertificate(location, value, errors) { - if (type(value) != "string") - push(errors, [ location, "must be of type string" ]); + return value; + } - return value; - } + if (exists(value, "use-local-certificates")) { + obj.use_local_certificates = parseUseLocalCertificates(location + "/use-local-certificates", value["use-local-certificates"], errors); + } + else { + obj.use_local_certificates = false; + } - if (exists(value, "ca-certificate")) { - obj.ca_certificate = parseCaCertificate(location + "/ca-certificate", value["ca-certificate"], errors); - } + function parseCaCertificate(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); - function parseCertificate(location, value, errors) { - if (type(value) != "string") - push(errors, [ location, "must be of type string" ]); + return value; + } - return value; - } + if (exists(value, "ca-certificate")) { + obj.ca_certificate = parseCaCertificate(location + "/ca-certificate", value["ca-certificate"], errors); + } - if (exists(value, "certificate")) { - obj.certificate = parseCertificate(location + "/certificate", value["certificate"], errors); - } + function parseCertificate(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); - function parsePrivateKey(location, value, errors) { - if (type(value) != "string") - push(errors, [ location, "must be of type string" ]); + return value; + } - return value; - } + if (exists(value, "certificate")) { + obj.certificate = parseCertificate(location + "/certificate", value["certificate"], errors); + } + + function parsePrivateKey(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } + + if (exists(value, "private-key")) { + obj.private_key = parsePrivateKey(location + "/private-key", value["private-key"], errors); + } + + function parsePrivateKeyPassword(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } - if (exists(value, "private-key")) { - obj.private_key = parsePrivateKey(location + "/private-key", value["private-key"], errors); + if (exists(value, "private-key-password")) { + obj.private_key_password = parsePrivateKeyPassword(location + "/private-key-password", value["private-key-password"], errors); + } + + return obj; } - function parsePrivateKeyPassword(location, value, errors) { - if (type(value) != "string") - push(errors, [ location, "must be of type string" ]); + if (type(value) != "object") + push(errors, [ location, "must be of type object" ]); - return value; + return value; + } + + function parseVariant1(location, value, errors) { + if (type(value) == "object") { + let obj = {}; + + function parseProtocol(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + if (!(value in [ "radius" ])) + push(errors, [ location, "must be one of \"radius\"" ]); + + return value; + } + + if (exists(value, "protocol")) { + obj.protocol = parseProtocol(location + "/protocol", value["protocol"], errors); + } + + function parseRealm(location, value, errors) { + if (type(value) == "array") { + function parseItem(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } + + return map(value, (item, i) => parseItem(location + "/" + i, item, errors)); + } + + if (type(value) != "array") + push(errors, [ location, "must be of type array" ]); + + return value; + } + + if (exists(value, "realm")) { + obj.realm = parseRealm(location + "/realm", value["realm"], errors); + } + + function parseAuthServer(location, value, errors) { + if (type(value) == "string") { + if (!matchUcHost(value)) + push(errors, [ location, "must be a valid hostname or IP address" ]); + + } + + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } + + if (exists(value, "auth-server")) { + obj.auth_server = parseAuthServer(location + "/auth-server", value["auth-server"], errors); + } + + function parseAuthPort(location, value, errors) { + if (type(value) in [ "int", "double" ]) { + if (value > 65535) + push(errors, [ location, "must be lower than or equal to 65535" ]); + + if (value < 1024) + push(errors, [ location, "must be bigger than or equal to 1024" ]); + + } + + if (type(value) != "int") + push(errors, [ location, "must be of type integer" ]); + + return value; + } + + if (exists(value, "auth-port")) { + obj.auth_port = parseAuthPort(location + "/auth-port", value["auth-port"], errors); + } + + function parseAuthSecret(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } + + if (exists(value, "auth-secret")) { + obj.auth_secret = parseAuthSecret(location + "/auth-secret", value["auth-secret"], errors); + } + + function parseAcctServer(location, value, errors) { + if (type(value) == "string") { + if (!matchUcHost(value)) + push(errors, [ location, "must be a valid hostname or IP address" ]); + + } + + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } + + if (exists(value, "acct-server")) { + obj.acct_server = parseAcctServer(location + "/acct-server", value["acct-server"], errors); + } + + function parseAcctPort(location, value, errors) { + if (type(value) in [ "int", "double" ]) { + if (value > 65535) + push(errors, [ location, "must be lower than or equal to 65535" ]); + + if (value < 1024) + push(errors, [ location, "must be bigger than or equal to 1024" ]); + + } + + if (type(value) != "int") + push(errors, [ location, "must be of type integer" ]); + + return value; + } + + if (exists(value, "acct-port")) { + obj.acct_port = parseAcctPort(location + "/acct-port", value["acct-port"], errors); + } + + function parseAcctSecret(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } + + if (exists(value, "acct-secret")) { + obj.acct_secret = parseAcctSecret(location + "/acct-secret", value["acct-secret"], errors); + } + + return obj; } - if (exists(value, "private-key-password")) { - obj.private_key_password = parsePrivateKeyPassword(location + "/private-key-password", value["private-key-password"], errors); + if (type(value) != "object") + push(errors, [ location, "must be of type object" ]); + + return value; + } + + function parseVariant2(location, value, errors) { + if (type(value) == "object") { + let obj = {}; + + function parseProtocol(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + if (!(value in [ "block" ])) + push(errors, [ location, "must be one of \"block\"" ]); + + return value; + } + + if (exists(value, "protocol")) { + obj.protocol = parseProtocol(location + "/protocol", value["protocol"], errors); + } + + function parseRealm(location, value, errors) { + if (type(value) == "array") { + function parseItem(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } + + return map(value, (item, i) => parseItem(location + "/" + i, item, errors)); + } + + if (type(value) != "array") + push(errors, [ location, "must be of type array" ]); + + return value; + } + + if (exists(value, "realm")) { + obj.realm = parseRealm(location + "/realm", value["realm"], errors); + } + + function parseMessage(location, value, errors) { + if (type(value) == "array") { + function parseItem(location, value, errors) { + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } + + return map(value, (item, i) => parseItem(location + "/" + i, item, errors)); + } + + if (type(value) != "string") + push(errors, [ location, "must be of type string" ]); + + return value; + } + + if (exists(value, "message")) { + obj.message = parseMessage(location + "/message", value["message"], errors); + } + + return obj; } - return obj; + if (type(value) != "object") + push(errors, [ location, "must be of type object" ]); + + return value; } - if (type(value) != "object") - push(errors, [ location, "must be of type object" ]); + let success = 0, tryval, tryerr, vvalue = null, verrors = []; + + tryerr = []; + tryval = parseVariant0(location, value, tryerr); + if (!length(tryerr)) { + if (type(vvalue) == "object" && type(tryval) == "object") + vvalue = { ...vvalue, ...tryval }; + else + vvalue = tryval; + + success++; + } + else { + push(verrors, join(" and\n", map(tryerr, err => "\t - " + err[1]))); + } + + tryerr = []; + tryval = parseVariant1(location, value, tryerr); + if (!length(tryerr)) { + if (type(vvalue) == "object" && type(tryval) == "object") + vvalue = { ...vvalue, ...tryval }; + else + vvalue = tryval; + + success++; + } + else { + push(verrors, join(" and\n", map(tryerr, err => "\t - " + err[1]))); + } + + tryerr = []; + tryval = parseVariant2(location, value, tryerr); + if (!length(tryerr)) { + if (type(vvalue) == "object" && type(tryval) == "object") + vvalue = { ...vvalue, ...tryval }; + else + vvalue = tryval; + + success++; + } + else { + push(verrors, join(" and\n", map(tryerr, err => "\t - " + err[1]))); + } + + if (success == 0) { + if (length(verrors)) + push(errors, [ location, "must match at least one of the following constraints:\n" + join("\n- or -\n", verrors) ]); + else + push(errors, [ location, "must match only one variant" ]); + return null; + } + + value = vvalue; return value; } diff --git a/ucentral.schema.json b/ucentral.schema.json index c4b5fb3..62bd1b9 100644 --- a/ucentral.schema.json +++ b/ucentral.schema.json @@ -2055,48 +2055,147 @@ "realms": { "type": "array", "items": { - "type": "object", - "properties": { - "realm": { - "type": "string", - "default": "*" - }, - "auto-discover": { - "type": "boolean", - "default": false - }, - "host": { - "type": "string", - "format": "uc-host", - "examples": [ - "192.168.1.10" - ] - }, - "port": { - "type": "integer", - "maximum": 65535, - "default": 2083 - }, - "secret": { - "type": "string" - }, - "use-local-certificates": { - "type": "boolean", - "default": false - }, - "ca-certificate": { - "type": "string" - }, - "certificate": { - "type": "string" + "anyOf": [ + { + "type": "object", + "properties": { + "protocol": { + "type": "string", + "enum": [ + "radsec" + ], + "default": "radsec" + }, + "realm": { + "type": "array", + "items": { + "type": "string", + "default": "*" + } + }, + "auto-discover": { + "type": "boolean", + "default": false + }, + "host": { + "type": "string", + "format": "uc-host", + "examples": [ + "192.168.1.10" + ] + }, + "port": { + "type": "integer", + "maximum": 65535, + "default": 2083 + }, + "secret": { + "type": "string" + }, + "use-local-certificates": { + "type": "boolean", + "default": false + }, + "ca-certificate": { + "type": "string" + }, + "certificate": { + "type": "string" + }, + "private-key": { + "type": "string" + }, + "private-key-password": { + "type": "string" + } + } }, - "private-key": { - "type": "string" + { + "type": "object", + "properties": { + "protocol": { + "type": "string", + "enum": [ + "radius" + ] + }, + "realm": { + "type": "array", + "items": { + "type": "string", + "default": "*" + } + }, + "auth-server": { + "type": "string", + "format": "uc-host", + "examples": [ + "192.168.1.10" + ] + }, + "auth-port": { + "type": "integer", + "maximum": 65535, + "minimum": 1024, + "examples": [ + 1812 + ] + }, + "auth-secret": { + "type": "string", + "examples": [ + "secret" + ] + }, + "acct-server": { + "type": "string", + "format": "uc-host", + "examples": [ + "192.168.1.10" + ] + }, + "acct-port": { + "type": "integer", + "maximum": 65535, + "minimum": 1024, + "examples": [ + 1812 + ] + }, + "acct-secret": { + "type": "string", + "examples": [ + "secret" + ] + } + } }, - "private-key-password": { - "type": "string" + { + "type": "object", + "properties": { + "protocol": { + "type": "string", + "enum": [ + "block" + ] + }, + "realm": { + "type": "array", + "items": { + "type": "string", + "default": "*" + } + }, + "message": { + "type": "string", + "items": { + "type": "string", + "default": "blocked" + } + } + } } - } + ] } } }