From: Arran Cudbard-Bell Date: Tue, 23 May 2023 22:29:22 +0000 (-0400) Subject: Add HTTP <-> RADIUS healthcheck gateway X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c671edbb1a6b36089595c9c21ae617db5c15e1dd;p=thirdparty%2Ffreeradius-server.git Add HTTP <-> RADIUS healthcheck gateway --- diff --git a/scripts/health/radhttpcheck/README.md b/scripts/health/radhttpcheck/README.md new file mode 100644 index 00000000000..74aa04f298b --- /dev/null +++ b/scripts/health/radhttpcheck/README.md @@ -0,0 +1,108 @@ +# radhttpcheck +## Introduction +No cloud providers currently support sending RADIUS packets from their cloud native load balancers. + +In order to actively monitor FreeRADIUS (or other RADIUS servers) in these environments, we need to +provide a HTTP service which sends RADIUS packets on behalf of the load balancer + +This script provides a HTTP <-> RADIUS gateway, sending pre-configured RADIUS packets to a IP/Port +and translating the response codes in HTTP response codes. + +## How it works +The configuration file allows one or more healthchecks to be configured, these healthchecks, when accessed +with HTTP GET, send a RADIUS request to a given ip/port (usually localhost, port 1812/1813). + +The underlying HTTP library forks a new thread for each access, and each thread opens a new UDP +socket to ensure there are never conflicting source port or IP allocations. + +No caching is performed, and each HTTP GET results in a new RADIUS packet being sent. One or more +retries can be configured, with an N second timeout. + +The process is entirely synchronous, which, given the relatively low volume of requests, is fine, +but you should ensure the healthcheck server is NOT accessible from the wider internet. + +## Configuration +By default this script loads its configuration from `radhttpcheck.yml` + +### Example +```yaml +listen: + # Address we bind to + address: '*' + # HTTP port to listen on + port: 8080 +# URLs the healthcheck script will respond on, and the various types of requests they create +healthchecks: + '/acct': + port: 1813 + secret: testing123 + type: Accounting-Request + retries: 3 + timeout: 1 + attributes: + Acct-Session-Id: '0123456789' + Acct-Status-Type: 'Start' + '/auth': + port: 1812 + secret: testing123 + type: Access-Request + '/customEndpoint': + port: 101812 + secret: foo + type: 29 +dictionary: /usr/local/radhttpcheck/dictionary +``` + +### `listen` +| attr | default | comment | +|---------------|------------------|----------------------------------------------------------| +| `address` | `*` | Interface we listen for HTTP requests on | +| `port` | `8080` | Port we listen for HTTP requests on | + +### `healthchecks` + +healthchecks is a dictionary with keys representing the URL that will trigger the healthcheck +and a dict containing the healthcheck configuration. + +| attr | default | comment | +|---------------|------------------|----------------------------------------------------------| +| `port` | set by type | UDP port we send RADIUS requests to | +| `secret` | `testing123` | RADIUS shared secret | +| `type` | set by port | Request packet type, `Access-Request`, `Accounting-Request`, `CoA-Request`, `Disconnect-Request`, `Status-Server`, or the packet code as an integer value | +| `retries` | `1` | How many times we resend the request on timeout | +| `timeout` | `1` | How long we wait for a response | +| `attributes` | `{}` | A dictionary of RADIUS attributes to send in the request, each attribute can be sent once | +| `require_ack` | False | Whether we require a positive acknowledgement i.e. `Access-Accept` for `Access-Request`, `CoA-ACK` for `CoA-Request` to count the healthcheck as successful. When `False`, any response is OK | + +### `dictionary` + +The path to the RADIUS attribute dictionary file, defaults to `dictionary`. + +## Dictionary format and contents + +A pyrad compatible dictionary file 'dictionary' is available in this directory. This is the aggregate +of RFC 2865, 2866, and 2869 with any FreeRADIUS v4 syntax that PyRad didn't like removed. + +You may customise it to add additional vendor attributes, but be aware PyRad uses the old style v3 +dictionary format. + +## HTTP response codes + +As this script mostly acts as a gateway between the HTTP client, and RADIUS server, HTTP gateway response +codes are used to indicate errors. + +| code | meaning | comment | +|---------------|-------------------|----------------------------------------------------------| +| `200` | Success | We received a valid response from the RADIUS server | +| `500` | Script failure | An internal error ocurred in the healthcheck script | +| `502` | Invalid response | Either the response packet was malformed or failed validation (bad shared secret), or `require_ack` was enabled, and the response contained a NAK response like `Access-Reject` | +| `504` | Timeout | No response received from the RADIUS server | + +In all cases a JSON blob will be received in the format `{ 'msg": "" }` + +## Built-in HTTP endpoints + +| endpoint | usage | comment | +|---------------|-------------------|----------------------------------------------------------| +| `/alwaysOk` | CoA/DM src ports | Sometimes CoA/DM src ports need to be routed via a loadbalancer, this endpoint ensures the healthchecks never fail so long as the CoA/DM server is reachable | +| `/list` | Show healthchecks | List all the available healthchecks | diff --git a/scripts/health/radhttpcheck/dictionary b/scripts/health/radhttpcheck/dictionary new file mode 100644 index 00000000000..4d5f0f3ecb3 --- /dev/null +++ b/scripts/health/radhttpcheck/dictionary @@ -0,0 +1,223 @@ +# -*- text -*- +# Copyright (C) 2023 The FreeRADIUS Server project and contributors +# This work is licensed under CC-BY version 4.0 https://creativecommons.org/licenses/by/4.0 +# Version $Id$ +# +# This is a modified version of the RFC 2865/2866/2869 dictionaries which works with PyRad +# +# $Id$ +# +ATTRIBUTE User-Name 1 string +ATTRIBUTE User-Password 2 string encrypt=1 +ATTRIBUTE CHAP-Password 3 octets +ATTRIBUTE NAS-IP-Address 4 ipaddr +ATTRIBUTE NAS-Port 5 integer +ATTRIBUTE Service-Type 6 integer +ATTRIBUTE Framed-Protocol 7 integer +ATTRIBUTE Framed-IP-Address 8 ipaddr +ATTRIBUTE Framed-IP-Netmask 9 ipaddr +ATTRIBUTE Framed-Routing 10 integer +ATTRIBUTE Filter-Id 11 string +ATTRIBUTE Framed-MTU 12 integer +ATTRIBUTE Framed-Compression 13 integer +ATTRIBUTE Login-IP-Host 14 ipaddr +ATTRIBUTE Login-Service 15 integer +ATTRIBUTE Login-TCP-Port 16 integer +# Attribute 17 is undefined +ATTRIBUTE Reply-Message 18 string +ATTRIBUTE Callback-Number 19 string +ATTRIBUTE Callback-Id 20 string +# Attribute 21 is undefined +ATTRIBUTE Framed-Route 22 string +ATTRIBUTE Framed-IPX-Network 23 ipaddr +ATTRIBUTE State 24 octets +ATTRIBUTE Class 25 octets + +ATTRIBUTE Session-Timeout 27 integer +ATTRIBUTE Idle-Timeout 28 integer +ATTRIBUTE Termination-Action 29 integer +ATTRIBUTE Called-Station-Id 30 string +ATTRIBUTE Calling-Station-Id 31 string +ATTRIBUTE NAS-Identifier 32 string +ATTRIBUTE Proxy-State 33 octets +ATTRIBUTE Login-LAT-Service 34 string +ATTRIBUTE Login-LAT-Node 35 string +ATTRIBUTE Login-LAT-Group 36 octets +ATTRIBUTE Framed-AppleTalk-Link 37 integer +ATTRIBUTE Framed-AppleTalk-Network 38 integer +ATTRIBUTE Framed-AppleTalk-Zone 39 string + +ATTRIBUTE Acct-Status-Type 40 integer +ATTRIBUTE Acct-Delay-Time 41 integer +ATTRIBUTE Acct-Input-Octets 42 integer +ATTRIBUTE Acct-Output-Octets 43 integer +ATTRIBUTE Acct-Session-Id 44 string +ATTRIBUTE Acct-Authentic 45 integer +ATTRIBUTE Acct-Session-Time 46 integer +ATTRIBUTE Acct-Input-Packets 47 integer +ATTRIBUTE Acct-Output-Packets 48 integer +ATTRIBUTE Acct-Terminate-Cause 49 integer +ATTRIBUTE Acct-Multi-Session-Id 50 string +ATTRIBUTE Acct-Link-Count 51 integer + +# Accounting Status Types + +VALUE Acct-Status-Type Start 1 +VALUE Acct-Status-Type Stop 2 +VALUE Acct-Status-Type Alive 3 # dup +VALUE Acct-Status-Type Interim-Update 3 +VALUE Acct-Status-Type Accounting-On 7 +VALUE Acct-Status-Type Accounting-Off 8 +VALUE Acct-Status-Type Failed 15 + +# Authentication Types + +VALUE Acct-Authentic RADIUS 1 +VALUE Acct-Authentic Local 2 +VALUE Acct-Authentic Remote 3 +VALUE Acct-Authentic Diameter 4 + +# Acct Terminate Causes + +VALUE Acct-Terminate-Cause User-Request 1 +VALUE Acct-Terminate-Cause Lost-Carrier 2 +VALUE Acct-Terminate-Cause Lost-Service 3 +VALUE Acct-Terminate-Cause Idle-Timeout 4 +VALUE Acct-Terminate-Cause Session-Timeout 5 +VALUE Acct-Terminate-Cause Admin-Reset 6 +VALUE Acct-Terminate-Cause Admin-Reboot 7 +VALUE Acct-Terminate-Cause Port-Error 8 +VALUE Acct-Terminate-Cause NAS-Error 9 +VALUE Acct-Terminate-Cause NAS-Request 10 +VALUE Acct-Terminate-Cause NAS-Reboot 11 +VALUE Acct-Terminate-Cause Port-Unneeded 12 +VALUE Acct-Terminate-Cause Port-Preempted 13 +VALUE Acct-Terminate-Cause Port-Suspended 14 +VALUE Acct-Terminate-Cause Service-Unavailable 15 +VALUE Acct-Terminate-Cause Callback 16 +VALUE Acct-Terminate-Cause User-Error 17 +VALUE Acct-Terminate-Cause Host-Request 18 + +ATTRIBUTE CHAP-Challenge 60 octets +ATTRIBUTE NAS-Port-Type 61 integer +ATTRIBUTE Port-Limit 62 integer +ATTRIBUTE Login-LAT-Port 63 string + +# +# Integer Translations +# + +# Service types + +VALUE Service-Type Login-User 1 +VALUE Service-Type Framed-User 2 +VALUE Service-Type Callback-Login-User 3 +VALUE Service-Type Callback-Framed-User 4 +VALUE Service-Type Outbound-User 5 +VALUE Service-Type Administrative-User 6 +VALUE Service-Type NAS-Prompt-User 7 +VALUE Service-Type Authenticate-Only 8 +VALUE Service-Type Callback-NAS-Prompt 9 +VALUE Service-Type Call-Check 10 +VALUE Service-Type Callback-Administrative 11 + +# Framed Protocols + +VALUE Framed-Protocol PPP 1 +VALUE Framed-Protocol SLIP 2 +VALUE Framed-Protocol ARAP 3 +VALUE Framed-Protocol Gandalf-SLML 4 +VALUE Framed-Protocol Xylogics-IPX-SLIP 5 +VALUE Framed-Protocol X.75-Synchronous 6 + +# Framed Routing Values + +VALUE Framed-Routing None 0 +VALUE Framed-Routing Broadcast 1 +VALUE Framed-Routing Listen 2 +VALUE Framed-Routing Broadcast-Listen 3 + +# Framed Compression Types + +VALUE Framed-Compression None 0 +VALUE Framed-Compression Van-Jacobson-TCP-IP 1 +VALUE Framed-Compression IPX-Header-Compression 2 +VALUE Framed-Compression Stac-LZS 3 + +# Login Services + +VALUE Login-Service Telnet 0 +VALUE Login-Service Rlogin 1 +VALUE Login-Service TCP-Clear 2 +VALUE Login-Service PortMaster 3 +VALUE Login-Service LAT 4 +VALUE Login-Service X25-PAD 5 +VALUE Login-Service X25-T3POS 6 +VALUE Login-Service TCP-Clear-Quiet 8 + +# Login-TCP-Port (see /etc/services for more examples) + +VALUE Login-TCP-Port Telnet 23 +VALUE Login-TCP-Port Rlogin 513 +VALUE Login-TCP-Port Rsh 514 + +# Termination Options + +VALUE Termination-Action Default 0 +VALUE Termination-Action RADIUS-Request 1 + +# NAS Port Types + +VALUE NAS-Port-Type Async 0 +VALUE NAS-Port-Type Sync 1 +VALUE NAS-Port-Type ISDN 2 +VALUE NAS-Port-Type ISDN-V120 3 +VALUE NAS-Port-Type ISDN-V110 4 +VALUE NAS-Port-Type Virtual 5 +VALUE NAS-Port-Type PIAFS 6 +VALUE NAS-Port-Type HDLC-Clear-Channel 7 +VALUE NAS-Port-Type X.25 8 +VALUE NAS-Port-Type X.75 9 +VALUE NAS-Port-Type G.3-Fax 10 +VALUE NAS-Port-Type SDSL 11 +VALUE NAS-Port-Type ADSL-CAP 12 +VALUE NAS-Port-Type ADSL-DMT 13 +VALUE NAS-Port-Type IDSL 14 +VALUE NAS-Port-Type Ethernet 15 +VALUE NAS-Port-Type xDSL 16 +VALUE NAS-Port-Type Cable 17 +VALUE NAS-Port-Type Wireless-Other 18 +VALUE NAS-Port-Type Wireless-802.11 19 + +ATTRIBUTE Acct-Input-Gigawords 52 integer +ATTRIBUTE Acct-Output-Gigawords 53 integer + +ATTRIBUTE Event-Timestamp 55 date + +ATTRIBUTE ARAP-Password 70 octets[16] +ATTRIBUTE ARAP-Features 71 octets[14] +ATTRIBUTE ARAP-Zone-Access 72 integer +ATTRIBUTE ARAP-Security 73 integer +ATTRIBUTE ARAP-Security-Data 74 string +ATTRIBUTE Password-Retry 75 integer +ATTRIBUTE Prompt 76 integer +ATTRIBUTE Connect-Info 77 string +ATTRIBUTE Configuration-Token 78 string +ATTRIBUTE EAP-Message 79 octets concat +ATTRIBUTE Message-Authenticator 80 octets + +ATTRIBUTE ARAP-Challenge-Response 84 octets[8] +ATTRIBUTE Acct-Interim-Interval 85 integer +# 86: RFC 2867 +ATTRIBUTE NAS-Port-Id 87 string +ATTRIBUTE Framed-Pool 88 string + +# ARAP Zone Access + +VALUE ARAP-Zone-Access Default-Zone 1 +VALUE ARAP-Zone-Access Zone-Filter-Inclusive 2 +VALUE ARAP-Zone-Access Zone-Filter-Exclusive 4 + +# Prompt +VALUE Prompt No-Echo 0 +VALUE Prompt Echo 1 diff --git a/scripts/health/radhttpcheck/radhttpcheck.py b/scripts/health/radhttpcheck/radhttpcheck.py new file mode 100755 index 00000000000..57658671ab0 --- /dev/null +++ b/scripts/health/radhttpcheck/radhttpcheck.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 + +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +# +# Copyright (C) 2023 Arran Cudbard-Bell +# + +from http.server import HTTPServer, BaseHTTPRequestHandler +from socketserver import ThreadingMixIn +import threading + +from pyrad.client import Client, Timeout +from pyrad.dictionary import Dictionary +import pyrad.packet + +import json +import yaml + +# Our configuration object +config = {} +raddict = {} + +class RadiusHealthCheckHandler(BaseHTTPRequestHandler): + def genericResponse(self, code, content): + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(bytes(content, 'utf8')) + + def codeToStr(self, code): + code_map = { + pyrad.packet.AccessRequest : 'Access-Request', + pyrad.packet.AccessAccept : 'Access-Accept', + pyrad.packet.AccessReject : 'Access-Reject', + pyrad.packet.AccountingRequest : 'Accounting-Request', + pyrad.packet.AccountingResponse : 'Accounting-Response', + pyrad.packet.AccessChallenge : 'Access-Challenge', + pyrad.packet.StatusServer : 'Status-Server', + pyrad.packet.StatusClient : 'Status-Client', + pyrad.packet.DisconnectRequest : 'Disconnect-Request', + pyrad.packet.DisconnectACK : 'Disconnect-Ack', + pyrad.packet.DisconnectNAK : 'Disconnect-NAK', + pyrad.packet.CoARequest : 'CoA-Request', + pyrad.packet.CoAACK : 'CoA-ACK', + pyrad.packet.CoANAK : 'CoA-NAK' + } + if code in code_map: + return code_map[code] + return str(code) + + def do_GET(self): + global config + + if self.path == '/alwaysOk': + self.genericResponse(200, json.dumps({"msg": "This healthcheck is always up, and should be used for RADIUS source ports (for CoA and DM) only"})) + return + + if self.path == '/list': + self.genericResponse(200, json.dumps(list(config.healthchecks.keys()))) + return + + if not self.path in config.healthchecks: + self.genericResponse(404, json.dumps({"msg": "Invalid healthcheck " + self.path + ". Configured healthchecks are \"" + ', '.join(config.healthchecks.keys()) + "\""})) + return + + # Send a RADIUS request + healthcheck = config.healthchecks[self.path] + + # Create a new client for every request, this ensures that for the lifetime of the client + # a unique source port is used, and so we don't have to care about synchronisation around + # the different client instances when multiple HTTP requests come in for the same + # healthcheck. + port = healthcheck['port'] + client = Client(server = healthcheck['server'], + secret = bytes(healthcheck['secret'], 'utf8'), + retries = healthcheck['retries'], + timeout = healthcheck['timeout'], + authport = port, acctport = port, coaport = port, + dict = config.raddict) + + # Create the RADIUS request + if healthcheck['type']['req_code'] == pyrad.packet.AccessRequest: + req = client.CreateAuthPacket(**healthcheck['attributes']) + elif healthcheck['type']['req_code'] == pyrad.packet.AccountingRequest: + req = client.CreateAcctPacket(**healthcheck['attributes']) + elif healthcheck['type']['req_code'] == pyrad.packet.CoARequest: + req = client.CreateCoAPacket(**healthcheck['attributes']) + elif healthcheck['type']['req_code'] == pyrad.packet.StatusServer: + req = client.CreateAuthPacket(code=pyrad.packet.StatusServer,**healthcheck['attributes']) + else: + req = client.CreatePacket(code=healthcheck['type']['req_code'],**healthcheck['attributes']) + + # There's no reason not to add this or to make it configurable + req.add_message_authenticator() + + # We now block until retries and timeout have expired + try: + rsp = client.SendPacket(req) + except pyrad.packet.PacketError as e: + self.genericResponse(502, json.dumps({"msg": "Healthcheck error: " + str(e) })) # BadGateway + except pyrad.client.Timeout as e: + self.genericResponse(504, json.dumps({"msg": "Healthcheck error: No response from upstream"})) # Gateway timeout + except Exception as e: + self.genericResponse(500, json.dumps({"msg": "Internal error: " + str(e) })) # Internal error + finally: + del client # Ensure the socket is closed in a timely fashion + return + + # Deal with response code mismatches + if healthcheck['require_ack'] and healthcheck['type'].has_key('rsp_code') and rsp.code != healthcheck['type']['rsp_code']: + self.genericResponse(502, json.dumps({"msg": "Healthcheck error: Bad response code, expected " + code2str(healthcheck['type']['rsp_code']) + ", got " + code2str(rsp.code) })) # BadGateway + return + + self.genericResponse(200, json.dumps({"msg": "Healthcheck OK" })) + +class Configuration: + def __init__(self, configuration_filename='radhttpcheck.yml'): + if configuration_filename is None: + raise ValueError("Configuration filename must be supplied") + self._configuration_filename = configuration_filename + self._config = {} + self.read_configuration() + + def read_configuration(self): + packet_types = { + 'access-request': { + 'req_code': pyrad.packet.AccessRequest, + 'rsp_code': pyrad.packet.AccessAccept + }, + 'accounting-request': { + 'req_code': pyrad.packet.AccountingRequest, + 'rsp_code': pyrad.packet.AccountingResponse + }, + 'coa-request': { + 'req_code': pyrad.packet.CoARequest, + 'rsp_code': pyrad.packet.CoAACK + }, + 'disconnect-request': { + 'req_code': pyrad.packet.DisconnectRequest, + 'rsp_code': pyrad.packet.DisconnectACK + }, + 'status-server': { + 'req_code': pyrad.packet.StatusServer + } + } + + with open(self._configuration_filename, 'r') as file: + our_conf = yaml.safe_load(file) + + # Ensure basic keys and structures exist + our_conf = { 'listen' : {}, 'healthchecks' : {}, 'dictionary' : 'dictionary' } | our_conf + + # Load in our modified default RADIUS dictionary. We do this here to avoid parsing the + # dictionary file on every request + self.raddict = Dictionary(our_conf['dictionary']) + + # Configure defaults for the HTTP listener + our_conf['listen'] = { 'port': 8080, 'ipaddr': '' } | our_conf['listen'] + + # SimpleHTTP tries to resolve '*' and fails. An empty string means bing to any interface + if our_conf['listen']['ipaddr'] == '*': + our_conf['listen']['ipaddr'] = '' + + # Setup packet-specific defaults on the healthchecks + for healthcheck in our_conf['healthchecks'].keys(): + options = our_conf['healthchecks'][healthcheck] + options['type'] = options['type'].lower() + # Set different defaults depending on whether this an Access-Request or something else + if ('port' in options and options['port'] == '1812') or ('type' in options and options['type'] == 'access-request'): + our_conf['healthchecks'][healthcheck] = { + 'port': 1812, + 'type': 'access-request', + } | options + else: + our_conf['healthchecks'][healthcheck] = { + 'port': 1813, + 'type': 'accounting-request', + } | options + + our_conf['healthchecks'][healthcheck] = { + 'server': '127.0.0.1', + 'retries': 1, + 'timeout': 1, + 'require_ack': False, + 'secret': 'testing123', + 'attributes': {}, + } | options + + # Make sure the packet type is sane + if not options['type'] in packet_types: + # If type is a number, allow it so we can send custom packets + if not options['type'].isnumeric(): + raise ValueError("healthcheck.type must be one of " + ', '.join(list(packet_types.keys()))) + our_conf['healthchecks'][healthcheck]['type'] = { 'req_code': int(options['type']) } + else: + our_conf['healthchecks'][healthcheck]['type'] = packet_types[options['type']] + + # Sanity check the attributes so we can fail early + for attr, value in our_conf['healthchecks'][healthcheck]['attributes'].items(): + if not attr in self.raddict: + raise ValueError("Failed resolving RADIUS attribute " + attr + " for healthcheck " + healthcheck) + + radattr = self.raddict[attr] + + # Resolve enums + if len(radattr.values) > 0: + if not radattr.values.HasForward(value): + raise ValueError("Failed resolving RADIUS attribute " + attr + " value " + str(value) + " for healthcheck " + healthcheck) + + # Set default healthcheck parameters + self._config = our_conf + + def __getattr__(self, name): + return self._config[name] + +class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): + """Handle requests in a separate thread.""" + +def main(): + global config + global raddict + + # Parse our configuration, setting defaults + config = Configuration() + + # Start the HTTP server + with ThreadedHTTPServer((config.listen['ipaddr'], config.listen['port']), RadiusHealthCheckHandler) as httpd: + print("RADIUS HTTP healthcheck server running on port", config.listen['port']) + try: + httpd.serve_forever() + # Catch the KeyboardInterrupt exception we get on sigint + except KeyboardInterrupt: + pass + finally: + httpd.server_close() + +if __name__ == "__main__": + main() diff --git a/scripts/health/radhttpcheck/radhttpcheck.service b/scripts/health/radhttpcheck/radhttpcheck.service new file mode 100644 index 00000000000..20450d5312b --- /dev/null +++ b/scripts/health/radhttpcheck/radhttpcheck.service @@ -0,0 +1,16 @@ +[Unit] +Description=radhttpcheck health probe translator +After=network-online.target +Documentation=raddhtpcheck/README.md + +[Service] +Type=exec +ExecStart=/usr/local/radhttpcheck/radhttpcheck.py +Restart=on-failure +RestartSec=5 + +# Allow binding to low ports like 80 +CapabilityBoundingSet=CAP_NET_BIND_SERVICE + +[Install] +WantedBy=multi-user.target diff --git a/scripts/health/radhttpcheck/radhttpcheck.yml b/scripts/health/radhttpcheck/radhttpcheck.yml new file mode 100644 index 00000000000..545cba34867 --- /dev/null +++ b/scripts/health/radhttpcheck/radhttpcheck.yml @@ -0,0 +1,17 @@ +# Address listened on for HTTP traffic +listen: + address: '*' + port: 8000 +# URLs the healthcheck script will respond on, and the various types of requests they create +endpoints: + '/acct': + port: 1813 + secret: testing123 + type: Accounting-Request + attributes: + Acct-Session-Id: '0123456789' + Acct-Status-Type: 'Start' + '/auth': + port: 1812 + secret: testing123 + type: Access-Request diff --git a/scripts/health/radhttpcheck/requirements.txt b/scripts/health/radhttpcheck/requirements.txt new file mode 100644 index 00000000000..04565f3f47e --- /dev/null +++ b/scripts/health/radhttpcheck/requirements.txt @@ -0,0 +1,4 @@ +pyyaml +pyrad +pyjson +simple-http-server