Running a Radius Server
UDP
Note
For a more secure alternative, see the RadSec section below.
There are two ways of running a server: sync or async.
Pick an implementation above and copy the code into your project. This should just work now if you have already installed pyrad2
:
You should see the logs:
2025-07-07 12:38:19.929 | INFO | pyrad2.server_async:connection_made:57 - [127.0.0.1:1812] Transport created
2025-07-07 12:38:19.929 | INFO | pyrad2.server_async:connection_made:57 - [127.0.0.1:1813] Transport created
2025-07-07 12:38:19.929 | INFO | pyrad2.server_async:connection_made:57 - [127.0.0.1:3799] Transport created
Warning
Sync support may be dropped in the future. We strongly recommend you use the async version.
Handling packets
Note
You may want to jump ahead to the client section in order to make a test request to your server.
Fundamentally, you have to subclass the pyrad2
server and implement four methods.
class MyRadiusServer(ServerAsync):
def handle_auth_packet(self, protocol, pkt, addr):
def handle_acct_packet(self, protocol, pkt, addr):
def handle_coa_packet(self, protocol:, pkt, addr):
def handle_disconnect_packet(self, protocol, pkt, addr):
When a packet arrives at these functions it has already been parsed, validated and instantiated into a pyrad2.packet.Packet class.
The example implementation provided simply logs details of the request it has just received and it's meant to illustrate the contents of the packet being received.
def handle_auth_packet(self, protocol, pkt, addr):
logger.info("Received an authentication request with id {}", pkt.id)
logger.info("Authenticator {}", pkt.authenticator.hex())
logger.info("Secret {}", pkt.secret)
logger.info("Attributes: ")
for attr in pkt.keys():
logger.info("{}: {}", attr, pkt[attr])
Replying
To reply a packet, you must create a reply packet using self.create_reply_packet
. The first argument is the packet you received and you can pass keyword arguments to populate the reply packet.
def handle_auth_packet(self, protocol, pkt, addr):
...
reply = self.create_reply_packet(
pkt,
**{
"Service-Type": "Framed-User",
"Framed-IP-Address": "192.168.0.1",
"Framed-IPv6-Prefix": "fc66::1/64",
},
)
reply.code = AccessAccept
protocol.send_response(reply, addr)
Lastly, you set the reply code. Possible reply codes can be imported from pyrad2.packet
.
AccessAccept = 2
AccessReject = 3
AccountingResponse = 5
StatusServer = 12
StatusClient = 13
DisconnectACK = 41
DisconnectNAK = 42
CoAACK = 44
CoANAK = 45
RadSec (Radius Over TLS)
Overview
Note
This feature implements RFC 6614 and it's currently experimental.
Generally speaking, the content of the RFC is simple. RADIUS is experiencing several shortcomings, such as its dependency on the unreliable transport protocol UDP and the lack of security for large parts of its packet payload. RADIUS security is based on the MD5 algorithm, which has been proven to be insecure.
RADSEC effectively means performing communications over TCP instead of UDP (generally on port 2083) and use TLS as a security layer.
RADSEC is the same as “Radius Over TLS” or Radius/TLS.
The default destination port number for RADIUS over TLS is TCP/2083 and there are no separate ports for authentication, accounting, and dynamic authorization changes. All the routing for packet handling is done internally.
The RadSec server and client follow the RFC and sets the default shared secret to radsec
.
Running a RadSec Server
We provide an example implementation in our examples folder. You can download it and place on your project folder.
Test SSL certificates can be downloaded from the certs folder.
The princinple is the same as the classic UDP server. You inherit from the base class and implement four methods.
from pyrad2.radsec.server import RadSecServer as BaseRadSecServer
class RadSecServer(BaseRadSecServer):
# You must implement these four methods
async def handle_access_request(self, packet: AuthPacket):
pass
async def handle_accounting(self, packet: AcctPacket):
pass
async def handle_disconnect(self, packet: CoAPacket):
pass
async def handle_coa(self, packet: CoAPacket):
pass
async def main():
hosts = {
"127.0.0.1": RemoteHost(name="localhost", address="127.0.0.1", secret=b"radsec")
}
THIS_FOLDER = os.path.dirname(os.path.abspath(__file__))
server = RadSecServer(
hosts=hosts,
dictionary=Dictionary(THIS_FOLDER + "/dictionary"),
certfile=THIS_FOLDER + "/certs/server/server.cert.pem",
keyfile=THIS_FOLDER + "/certs/server/server.key.pem",
ca_certfile=THIS_FOLDER + "/certs/ca/ca.cert.pem",
)
await server.run()
if __name__ == "__main__":
asyncio.run(main())
Note
RadSec server is only available in async form.
Running this file shows us the server ready to accept requests.