rlm_python3 behavior with multi-worker threads

Erdal Emlik erdalemlik at icloud.com
Wed Sep 24 19:35:55 UTC 2025


Hi Alan,
Thank you for detailed and quick response.
This is my default configuration, and this is my Python module.

I also use Python for accounting, since I send records to Kafka from within it, but I have never received any exceptions there.

Getting exceptions only in authentication is a bit confusing for me.

(I’m planning to switch to the Kafka module and I’m following the updates on that…)

def authenticate(p):
    result = None
    try:
        result = port_authentication(p)
    except Exception as e:
        logstash_logger.add_info_log(f"Authenticate Failed {e}")
        return radiusd.RLM_MODULE_OK


    # Check the port authentication processes and return reject or ok
    return result
def port_authentication(p):
    """
    Authenticate a port based on its inventory and active session information.
    Produce a record to a Kafka topic if the port is not correct.
    Return a radiusd module status code.
    """
    # Check if the port authentication is enabled
    port_authentication_enabled = check_port_authentication_enabled(p)

    # Port authentication is not enabled => allow access
    if not port_authentication_enabled:
        return radiusd.RLM_MODULE_OK

    # Check if the port information is correct. Just checking the hostname.
    port_hostname_match = check_port_hostname_match(p)
    
    if port_hostname_match:
        return radiusd.RLM_MODULE_OK
    else:
        produce_port_authentication_event(p)
        return radiusd.RLM_MODULE_REJECT

def check_port_hostname_match(p):
    """
    Return True if the port information matches the inventory, False otherwise.
    """
    return inventory_port_hostname_value(p) == request_port_hostname_value(p) 

def check_port_authentication_enabled(p):
    """
    Return True if the port information matches the inventory, False otherwise.
    """
    return bool(find_key(p, "InventoryPortInfo"))
 
def inventory_port_hostname_value(p):
    """
    Port information at the inventory. Hostname is the first part after split port with #
    """
    return find_key(p, "InventoryPortInfo").split('#')[0]

def request_port_hostname_value(p):
    """
    Check and return NAS-Port-Id and Calling-Station-Id values. If we have NAS-Port-Id value, returns NAS-Port-Id. 
    Otherwise, return Calling-Station-Id (or empty if that is not exist).
    Hostname is the first part after split port with #
    """
    if find_key(p, "NAS-Port-Id"):
        return find_key(p, "NAS-Port-Id").split('#')[0]
    return find_key(p, "Calling-Station-Id").split('#')[0]

def find_key(p, key):
    for [attribute, value] in p:
        if key == attribute:
            return value
    return ''
    
def create_invalidport_record(p):
    """
    Create shared Kafka log object.
    """
    invalidPortEvent = {
        "username": find_key(p, "User-Name"),
        "requestedPort": request_port_hostname_value(p),
        "inventoryPort": find_key(p, "InventoryPortInfo")
    }
    return json.dumps(invalidPortEvent)

def produce_port_authentication_event(p):
    shared_kafka_producer.produce(kafka_port_auth_topic, key=find_key(p, "User-Name"), value=create_invalidport_record(p))



authenticate {
        Auth-Type PAP {
        -pap { 
                noop = 1
                reject = 2
                fail = 3
            }
            if (noop) {
                    reject_attributes
                   update reply {
                                Reply-Message := "Login Failed. Please check your Username"
                                }
                        accept
                }
            if (reject) {
                    reject_attributes
                   update reply {
                                Reply-Message := "Login Failed. Please check your Password",
                                }
                        accept
                }
            if (fail) {
                    reject_attributes
                   update reply {
                                Reply-Message := "Login Failed. Please check your User",
                                }
                        accept
                }
                

            update request{
                InventoryPortInfo := "%{sqlro:SELECT VALUE FROM RADCHECK WHERE USERNAME = '%{User-Name}' AND ATTRIBUTE IN ('Calling-Station-Id', 'NAS-Port-Id') ORDER BY ATTRIBUTE DESC LIMIT 1}"
            }
                -python3{
                        reject = 2
                }
                if(reject){
                reject_attributes
                        update reply {
                                Reply-Message := "Login Failed. Please check your port"
                        }
                        accept
                }

                #accept
        }
}

As you see in the python im not doing any expensive things.
Best regards,
 
> On 24 Sep 2025, at 21:27, Alan DeKok <alan.dekok at inkbridge.io> wrote:
> 
> On Sep 24, 2025, at 2:18 PM, Erdal Emlik via Freeradius-Users <freeradius-users at lists.freeradius.org> wrote:
>> I’m using FreeRADIUS (version 3.2.4) with rlm_python3 for authentication.
> 
>  Why?
> 
>  The only time it's required to use Python or Perl is when you need to access an external API, and that API is only available via a Python or Perl library.
> 
>> I have the following question:
>> 
>> - Does FreeRADIUS’ thread pool affect rlm_python3 requests?
>> 
>> - Specifically, does each RADIUS worker call the same Python interpreter instance, or does each worker get its own Python context?( i mean 28 workers uses trying to use single instance at the same time right as doc says “item is GLOBAL TO THE SERVER.  That is, you cannot have two instances of the python module”)
> 
>  No, that isn't what the documentation says,  You've deleted part of it.
> 
>  Each thread has it's own Python interpreter.
> 
>  The document actually says:
> 
> 	#  Note that due to limitations on Python, this configuration
> 	#  item is GLOBAL TO THE SERVER.  That is, you cannot have two
> 	#  instances of the python module, each with a different path.
> 
>  i.e. you CAN have two different instances of the python module.  BUT both need to share the same path.
> 
>> Additionally, within the Python module, I log requests and sometimes see “queue full” messages.
> 
>  Because Python is slow, and is blocking the server.
> 
>> However, even when these messages appear, I observe that the Python module still returns a response to FreeRADIUS.
> 
>  No.
> 
>  Or, the Python module is returning SOME requests to the server.  Other requests are blocking for 30+ seconds in the Python module.
> 
>  Don't run Python.
> 
>> Within Python, I perform some comparisons and assign IPs accordingly, and even if a "queue full" exception occurs, FreeRADIUS still receives the IP assignment and returns a response without any issue.
> 
>  FreeRADIUS supports comparisons in unlang, and IP assignment in unlang.  It will be about 1000 times faster than Python.
> 
>  And no, I'm not exaggerating.
> 
>> - Is this “queue full” behavior related to the Python module itself, or is it due to FreeRADIUS threading/queue management?
> 
>  It's due to the extremely slow script you've created.
> 
> a) make the script faster
> 
> b) don't use Python.
> 
>  Pick one.
> 
>  Alan DeKok.
> 



More information about the Freeradius-Users mailing list