--- /dev/null
+# 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": "<extended response message>" }`
+
+## 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 |
--- /dev/null
+# -*- 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
--- /dev/null
+#!/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 <a.cudbardb@freeradius.org>
+#
+
+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()
--- /dev/null
+[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
--- /dev/null
+# 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
--- /dev/null
+pyyaml
+pyrad
+pyjson
+simple-http-server