Enable the server to originate CoA-Request && Disconnect-Request
authorAlan T. DeKok <aland@freeradius.org>
Fri, 2 Jan 2009 15:23:03 +0000 (16:23 +0100)
committerAlan T. DeKok <aland@freeradius.org>
Fri, 2 Jan 2009 15:23:03 +0000 (16:23 +0100)
This is a fairly large change in the server, but is protected
by WITH_COA, so you can build without it, if you want to do that.

15 files changed:
man/man5/unlang.5
raddb/proxy.conf
raddb/sites-available/originate-coa [new file with mode: 0644]
share/dictionary.freeradius.internal
src/include/radius.h
src/include/radiusd.h
src/include/realms.h
src/main/client.c
src/main/evaluate.c
src/main/event.c
src/main/listen.c
src/main/mainconfig.c
src/main/modcall.c
src/main/realms.c
src/main/util.c

index d6cfe43..3785191 100644 (file)
@@ -144,10 +144,11 @@ the current block.
 .DE
 
 The <list> can be one of "request", "reply", "proxy-request",
-"proxy-reply", or "control".  The "control" list is the list of
-attributes maintainted internally by the server that controls how the
-server processes the request.  Any attribute that does not go in a
-packet on the network will generally be placed in the "control" list.
+"proxy-reply", "coa", "disconnect", or "control".  The "control" list
+is the list of attributes maintainted internally by the server that
+controls how the server processes the request.  Any attribute that
+does not go in a packet on the network will generally be placed in the
+"control" list.
 
 For backwards compatibility with older versions, "check" is accepted
 as a synonym for "control".  The use of "check" is deprecated, and
@@ -158,6 +159,14 @@ EAP-TTLS), the inner tunnel session can also reference
 "outer.request", "outer.reply", and "outer.control".  Those references
 allow you to address the relevant list in the outer tunnel session.
 
+The "coa" and "disconnect" sections can only be used when the server
+receives an Access-Request or Accounting-Request.  Adding one or more
+attributes to either of the "coa" or "disconnect" list causes server
+to originate a CoA-Request or Disconnect-Request packet.  That packet
+is sent when the current Access-Request or Accounting-Request has been
+finished, and a reply sent to the NAS.  See
+raddb/sites-available/originate-coa for additional information.
+
 The only contents permitted in an "update" section are attributes and
 values.  The contents of the "update" section are described in the
 ATTRIBUTES section below.
index aa93498..6133d67 100644 (file)
@@ -114,6 +114,8 @@ home_server localhost {
        #       acct      - Handles Accounting-Request packets
        #       auth+acct - Handles Access-Request packets at "port",
        #                   and Accounting-Request packets at "port + 1"
+       #       coa       - Handles CoA-Request and Disconnect-Request packets.
+       #                   See also raddb/sites-available/originate-coa
        type = auth
 
        #
@@ -162,6 +164,7 @@ home_server localhost {
        #
        #  Usually 1812 for type "auth", and  1813 for type "acct".
        #  Older servers may use 1645 and 1646.
+       #  Use 3799 for type "coa"
        #
        port = 1812
 
@@ -330,6 +333,45 @@ home_server localhost {
        #
        #  Useful range of values: 3 to 10
        num_answers_to_alive = 3
+
+       #
+       #  The configuration items in the next sub-section are used ONLY
+       #  when "type = coa".  It is ignored for all other type of home
+       #  servers.
+       #
+       #  See RFC 5080 for the definitions of the following terms.
+       #  RAND is a function (internal to FreeRADIUS) returning
+       #  random numbers between -0.1 and +0.1
+       #
+       #  First Re-transmit occurs after:
+       #
+       #        RT = IRT + RAND*IRT
+       #
+       #  Subsequent Re-transmits occur after:
+       #
+       #       RT = 2 * RTprev + RAND * RTprev
+       #
+       #  Re-trasnmits are capped at:
+       #
+       #       if (MRT && (RT > MRT)) RT = MRT + RAND * MRT
+       #
+       #  For a maximum number of attempts: MRC
+       #
+       #  For a maximum (total) period of time: MRD.
+       #
+       coa {
+               # Initial retransmit interval: 1..5
+               irt = 2
+
+               # Maximum Retransmit Timeout: 1..30 (0 == no maximum)
+               mrt = 16
+
+               # Maximum Retransmit Count: 1..20 (0 == retransmit forever)
+               mrc = 5
+
+               # Maximum Retransmit Duration: 5..60
+               mrd = 30
+       }
 }
 
 # Sample virtual home server.
diff --git a/raddb/sites-available/originate-coa b/raddb/sites-available/originate-coa
new file mode 100644 (file)
index 0000000..0124117
--- /dev/null
@@ -0,0 +1,190 @@
+# -*- text -*-
+######################################################################
+#
+#  The server can originate Change of Authorization (CoA) or
+#  Disconnect request packets.  These packets are used to dynamically
+#  change the parameters of a users session (bandwidth, etc.), or
+#  to forcibly disconnect the user.
+#
+#  There are some caveats.  Not all NAS vendors support this
+#  functionality.  Even for the ones that do, it may be difficult to
+#  find out what needs to go into a CoA-Request or Disconnect-Request
+#  packet.  All we can suggest is to read the NAS documentation
+#  available from the vendor.  That documentation SHOULD describe
+#  what information their equipment needs to see in a CoA packet.
+#
+#  This information is usually a list of attributes such as:
+#
+#      NAS-IP-Address (or NAS-IPv6 address)
+#      NAS-Identifier
+#      User-Name
+#      Acct-Session-Id
+#
+#  CoA packets can be originated when a normal Access-Request or
+#  Accounting-Request packet is received.  Simply update the
+#  "coa" list:
+#
+#      update coa {
+#             User-Name = "%{User-Name}"
+#             Acct-Session-Id = "%{Acct-Session-Id}"
+#             NAS-IP-Address = "%{NAS-IP-Address}"
+#      }
+#
+#  And the CoA packet will be sent.  You can also send Disconnect
+#  packets by using "update disconnect { ...".
+#
+#  This "update coa" entry can be placed in any section (authorize,
+#  preacct, etc.), EXCEPT for pre-proxy and post-proxy.  The CoA
+#  packets CANNOT be sent if the original request has been proxied.
+#
+#  The CoA functionality works best when the RADIUS server and 
+#  the NAS receiving CoA packets are on the same network.
+#
+#  If "update coa { ... " is used, and then later it becomes necessary
+#  to not send a CoA request, the following example can suppress the
+#  CoA packet:
+#
+#      update control {
+#              Send-CoA-Request = No
+#      }
+#
+#  The default destination of a CoA packet is the NAS (or client)
+#  the sent the original Access-Request or Accounting-Request.  See
+#  raddb/clients.conf for a "coa_server" configuration that ties
+#  a client to a specific home server, or to a home server pool.
+#
+#  If you need to send the packet to a different destination, update
+#  the "coa" list with one of:
+#
+#      Packet-Dst-IP-Address = ...
+#      Packet-Dst-IPv6-Address = ...
+#      Home-Server-Pool = ...
+#
+#  That specifies an Ipv4 or IPv6 address, or a home server pool
+#  (such as the "coa" pool example below).  This use is not
+#  recommended, however,  It is much better to point the client
+#  configuration directly at the CoA server/pool, as outlined
+#  earlier.
+#
+#  If the CoA port is non-standard, you can also set:
+#
+#      Packet-Dst-Port
+#
+#  to have the value of the port.
+#
+######################################################################
+
+#
+#  When CoA packets are sent to a NAS, the NAS is acting as a
+#  server (see RFC 5176).  i.e. it has a type (accepts CoA and/or
+#  Disconnect packets), an IP address (or IPv6 address), a
+#  destination port, and a shared secret.
+#
+#  This information *cannot* go into a "client" section.  In the future,
+#  FreeRADIUS will be able to receive, and to proxy CoA packets.
+#  Having the CoA configuration as below means that we can later do
+#  load-balancing, fail-over, etc. of CoA servers.  If the CoA
+#  configuration went into a "client" section, it would be impossible
+#  to do proper proxying of CoA requests.
+#
+home_server localhost-coa {
+       type = coa
+
+       #
+       #  Note that a home server of type "coa" MUST be a real NAS,
+       #  with an ipaddr or ipv6addr.  It CANNOT point to a virtual
+       #  server.
+       #
+       ipaddr = 127.0.0.1
+       port = 3799
+
+       #  This secret SHOULD NOT be the same as the shared
+       #  secret in a "client" section.
+       secret = testing1234
+
+       #  CoA specific parameters.  See raddb/proxy.conf for details.
+       coa {
+               irt = 2
+               mrt = 16
+               mrc = 5
+               mrd = 30
+       }
+}
+
+#
+#  CoA servers can be put into pools, just like normal servers.
+#
+home_server_pool coa {
+       type = fail-over
+
+       # Point to the CoA server aboce.
+       home_server = localhost-coa
+
+       #  CoA requests are run through the pre-proxy section.
+       #  CoA responses are run through the post-proxy section.
+       virtual_server = originate-coa.example.com
+
+       #
+       #  Home server pools of type "coa" cannot (currently) have
+       #  a "fallback" configuration.
+       #
+}
+
+#
+#  When this virtual server is run, the original request has FINISHED
+#  processing.  i.e. the reply has already been sent to the NAS.
+#  You can access the attributes in the original packet, reply, and
+#  control items, but changing them will have NO EFFECT.
+#
+#  The CoA packet is in the "proxy-request" attribute list.
+#  The CoA reply (if any) is in the "proxy-reply" attribute list.
+#
+server originate-coa.example.com {
+  pre-proxy {
+       update proxy-request {
+               NAS-IP-Address = 127.0.0.1
+       }
+  }
+
+  #
+  # Handle the responses here.
+  #
+  post-proxy {
+       switch "%{Response-Packet-Type}" {
+               case CoA-ACK {
+                       ok
+               }
+
+               case CoA-NAK {
+                       # the NAS didn't like the CoA request
+                       ok
+               }
+
+               case Disconnect-ACK {
+                       ok
+               }
+
+               case Disconnect-NAK {
+                       # the NAS didn't like the Disconnect request
+                       ok
+               }
+
+               # Invalid packet type.  This shouldn't happen.
+               case {
+                    fail
+               }
+       }
+
+       #
+       #  These methods are run when there is NO response
+       #  to the request.
+       #
+       Post-Proxy-Type Fail-CoA {
+               ok
+       }
+
+       Post-Proxy-Type Fail-Disconnect {
+               ok
+       }
+  }
+}
index 8a5b569..ec81886 100644 (file)
@@ -155,6 +155,12 @@ VALUE      EAP-IKEv2-AuthType              secret                  1
 VALUE  EAP-IKEv2-AuthType              cert                    2
 VALUE  EAP-IKEv2-AuthType              both                    3
 
+ATTRIBUTE      Send-Disconnect-Request                 1107    integer
+ATTRIBUTE      Send-CoA-Request                        1107    integer
+
+VALUE  Send-CoA-Request                No                      0
+VALUE  Send-CoA-Request                Yes                     1
+
 ATTRIBUTE      Module-Return-Code                      1108    integer
 
 VALUE  Module-Return-Code              reject                  0
@@ -169,6 +175,7 @@ VALUE       Module-Return-Code              updated                 8
 
 ATTRIBUTE      Packet-Original-Timestamp               1109    date
 ATTRIBUTE      SQL-Table-Name                          1110    string
+ATTRIBUTE      Home-Server-Pool                        1111    string
 
 ATTRIBUTE      FreeRADIUS-Client-IP-Address            1120    ipaddr
 ATTRIBUTE      FreeRADIUS-Client-IPv6-Address          1121    ipv6addr
@@ -460,6 +467,12 @@ VALUE      Response-Packet-Type            Access-Challenge        11
 VALUE  Response-Packet-Type            Status-Server           12
 VALUE  Response-Packet-Type            Status-Client           13
 
+VALUE  Response-Packet-Type            Disconnect-Request      40
+VALUE  Response-Packet-Type            Disconnect-ACK          41
+VALUE  Response-Packet-Type            Disconnect-NAK          42
+VALUE  Response-Packet-Type            CoA-Request             43
+VALUE  Response-Packet-Type            CoA-ACK                 44
+VALUE  Response-Packet-Type            CoA-NAK                 45
 #
 #  Special value
 #
index 2add98e..e671b13 100644 (file)
@@ -45,6 +45,7 @@
 #define PW_AUTH_UDP_PORT                1812
 #define PW_ACCT_UDP_PORT                1813
 #define PW_POD_UDP_PORT                        1700
+#define PW_COA_UDP_PORT                        3799
 
 #define        PW_USER_NAME                    1
 #define        PW_USER_PASSWORD                2
 #define PW_VIRTUAL_SERVER              1099
 #define PW_CLEARTEXT_PASSWORD          1100
 #define PW_PASSWORD_WITH_HEADER                1101
+#define PW_SEND_COA_REQUEST            1107
 #define PW_MODULE_RETURN_CODE          1108
 #define PW_PACKET_ORIGINAL_TIMESTAMP           1109
+#define PW_HOME_SERVER_POOL            1111
 
 /*
  *     Integer Translations
index 52b5cbf..efe36f7 100644 (file)
@@ -92,6 +92,13 @@ typedef struct auth_req REQUEST;
 #endif
 #endif
 
+#ifndef WITHOUT_COA
+#define WITH_COA (1)
+#ifndef WITH_PROXY
+#error WITH_COA requires WITH_PROXY
+#endif
+#endif
+
 #include <freeradius-devel/stats.h>
 #include <freeradius-devel/realms.h>
 
@@ -128,6 +135,12 @@ typedef struct radclient {
        time_t                  last_new_client;
        char                    *client_server;
 #endif
+
+#ifdef WITH_COA
+       char                    *coa_name;
+       home_server             *coa_server;
+       home_pool_t             *coa_pool;
+#endif
 } RADCLIENT;
 
 /*
@@ -239,6 +252,10 @@ struct auth_req {
        const char              *server;
        REQUEST                 *parent;
        radlog_func_t           radlog; /* logging function, if set */
+#ifdef WITH_COA
+       REQUEST                 *coa;
+       int                     num_coa_requests;
+#endif
 };                             /* REQUEST typedef */
 
 #define RAD_REQUEST_OPTION_NONE            (0)
@@ -468,6 +485,7 @@ int         rad_checkfilename(const char *filename);
 void           *rad_malloc(size_t size); /* calls exit(1) on error! */
 REQUEST                *request_alloc(void);
 REQUEST                *request_alloc_fake(REQUEST *oldreq);
+REQUEST                *request_alloc_coa(REQUEST *request);
 int            request_data_add(REQUEST *request,
                                 void *unique_ptr, int unique_int,
                                 void *opaque, void (*free_opaque)(void *));
index 1e626d5..206be2f 100644 (file)
@@ -15,6 +15,9 @@ RCSIDH(realms_h, "$Id$")
 #define HOME_TYPE_INVALID (0)
 #define HOME_TYPE_AUTH    (1)
 #define HOME_TYPE_ACCT    (2)
+#ifdef WITH_COA
+#define HOME_TYPE_COA     (3)
+#endif
 
 #define HOME_PING_CHECK_NONE           (0)
 #define HOME_PING_CHECK_STATUS_SERVER  (1)
@@ -65,6 +68,12 @@ typedef struct home_server {
 
        int             revive_interval; /* if it doesn't support pings */
        CONF_SECTION    *cs;
+#ifdef WITH_COA
+       int                     coa_irt;
+       int                     coa_mrc;
+       int                     coa_mrt;
+       int                     coa_mrd;
+#endif
 #ifdef WITH_STATS
        int             number;
 
@@ -117,8 +126,12 @@ REALM *realm_find2(const char *name); /* ... with name taken from realm_find */
 
 home_server *home_server_ldb(const char *realmname, home_pool_t *pool, REQUEST *request);
 home_server *home_server_find(fr_ipaddr_t *ipaddr, int port);
+#ifdef WITH_COA
+home_server *home_server_byname(const char *name);
+#endif
 #ifdef WITH_STATS
 home_server *home_server_bynumber(int number);
 #endif
+home_pool_t *home_pool_byname(const char *name, int type);
 
 #endif /* REALMS_H */
index 0ea7961..debd5b8 100644 (file)
@@ -440,6 +440,11 @@ static const CONF_PARSER client_config[] = {
          offsetof(RADCLIENT, lifetime), 0, NULL },
 #endif
 
+#ifdef WITH_COA
+       { "coa_server",  PW_TYPE_STRING_PTR,
+         offsetof(RADCLIENT, coa_name), 0, NULL },
+#endif
+
        { NULL, -1, 0, NULL, NULL }
 };
 
@@ -629,6 +634,24 @@ static RADCLIENT *client_parse(CONF_SECTION *cs, int in_server)
                return NULL;
        }
 
+#ifdef WITH_COA
+       /*
+        *      Point the client to the home server pool, OR to the
+        *      home server.  This gets around the problem of figuring
+        *      out which port to use.
+        */
+       if (c->coa_name) {
+               c->coa_pool = home_pool_byname(c->coa_name, HOME_TYPE_COA);
+               if (!c->coa_pool) {
+                       c->coa_server = home_server_byname(c->coa_name);
+               }
+               if (!c->coa_server) {
+                       client_free(c);
+                       cf_log_err(cf_sectiontoitem(cs), "No such home_server or home_server_pool \"%s\"", c->coa_name);
+                       return NULL;
+               }
+       }
+#endif
 
        return c;
 }
index a3da0a0..4420e4e 100644 (file)
@@ -244,6 +244,42 @@ static int radius_get_vp(REQUEST *request, const char *name, VALUE_PAIR **vp_p)
                vp_name += 8;
                vps = myrequest->config_items;
 
+#ifdef WITH_COA
+       } else if (strncmp(vp_name, "coa:", 4) == 0) {
+               vp_name += 4;
+
+               if (myrequest->coa &&
+                   (myrequest->coa->proxy->code == PW_COA_REQUEST)) {
+                       vps = myrequest->coa->proxy->vps;
+               }
+
+       } else if (strncmp(vp_name, "coa-reply:", 10) == 0) {
+               vp_name += 10;
+
+               if (myrequest->coa && /* match reply with request */
+                   (myrequest->coa->proxy->code == PW_COA_REQUEST) &&
+                   (myrequest->coa->proxy_reply)) {
+                       vps = myrequest->coa->proxy_reply->vps;
+               }
+
+       } else if (strncmp(vp_name, "disconnect:", 11) == 0) {
+               vp_name += 11;
+
+               if (myrequest->coa &&
+                   (myrequest->coa->proxy->code == PW_DISCONNECT_REQUEST)) {
+                       vps = myrequest->coa->proxy->vps;
+               }
+
+       } else if (strncmp(vp_name, "disconnect-reply:", 17) == 0) {
+               vp_name += 17;
+
+               if (myrequest->coa && /* match reply with request */
+                   (myrequest->coa->proxy->code == PW_DISCONNECT_REQUEST) &&
+                   (myrequest->coa->proxy_reply)) {
+                       vps = myrequest->coa->proxy_reply->vps;
+               }
+#endif
+
        } else {
                vps = myrequest->packet->vps;
        }
@@ -845,6 +881,7 @@ static void fix_up(REQUEST *request)
        }
 }
 
+
 /*
  *     The pairmove() function in src/lib/valuepair.c does all sorts of
  *     extra magic that we don't want here.
@@ -1154,7 +1191,7 @@ int radius_update_attrlist(REQUEST *request, CONF_SECTION *cs,
        CONF_ITEM *ci;
        VALUE_PAIR *newlist, *vp;
        VALUE_PAIR **output_vps = NULL;
-       REQUEST *request_vps = request;
+       REQUEST *myrequest = request;
 
        if (!request || !cs) return RLM_MODULE_INVALID;
 
@@ -1165,29 +1202,67 @@ int radius_update_attrlist(REQUEST *request, CONF_SECTION *cs,
        if (strncmp(name, "outer.", 6) == 0) {
                if (!request->parent) return RLM_MODULE_NOOP;
 
-               request_vps = request->parent;
+               myrequest = request->parent;
                name += 6;
        }
 
        if (strcmp(name, "request") == 0) {
-               output_vps = &request_vps->packet->vps;
+               output_vps = &myrequest->packet->vps;
 
        } else if (strcmp(name, "reply") == 0) {
-               output_vps = &request_vps->reply->vps;
+               output_vps = &myrequest->reply->vps;
 
 #ifdef WITH_PROXY
        } else if (strcmp(name, "proxy-request") == 0) {
-               if (request->proxy) output_vps = &request_vps->proxy->vps;
+               if (request->proxy) output_vps = &myrequest->proxy->vps;
 
        } else if (strcmp(name, "proxy-reply") == 0) {
                if (request->proxy_reply) output_vps = &request->proxy_reply->vps;
 #endif
 
        } else if (strcmp(name, "config") == 0) {
-               output_vps = &request_vps->config_items;
+               output_vps = &myrequest->config_items;
 
        } else if (strcmp(name, "control") == 0) {
-               output_vps = &request_vps->config_items;
+               output_vps = &myrequest->config_items;
+
+#ifdef WITH_COA
+       } else if (strcmp(name, "coa") == 0) {
+               if (!myrequest->coa) {
+                       request_alloc_coa(myrequest);
+                       myrequest->coa->proxy->code = PW_COA_REQUEST;
+               }
+                 
+               if (myrequest->coa &&
+                   (myrequest->coa->proxy->code == PW_COA_REQUEST)) {
+                       output_vps = &myrequest->coa->proxy->vps;
+               }
+
+       } else if (strcmp(name, "coa-reply") == 0) {
+               if (myrequest->coa && /* match reply with request */
+                   (myrequest->coa->proxy->code == PW_COA_REQUEST) &&
+                    (myrequest->coa->proxy_reply)) {
+                     output_vps = &myrequest->coa->proxy_reply->vps;
+               }
+
+       } else if (strcmp(name, "disconnect") == 0) {
+               if (!myrequest->coa) {
+                       request_alloc_coa(myrequest);
+                       if (myrequest->coa) myrequest->coa->proxy->code = PW_DISCONNECT_REQUEST;
+               }
+
+               if (myrequest->coa &&
+                   (myrequest->coa->proxy->code == PW_DISCONNECT_REQUEST)) {
+                       output_vps = &myrequest->coa->proxy->vps;
+               }
+
+       } else if (strcmp(name, "disconnect-reply") == 0) {
+               if (myrequest->coa && /* match reply with request */
+                   (myrequest->coa->proxy->code == PW_DISCONNECT_REQUEST) &&
+                   (myrequest->coa->proxy_reply)) {
+                       output_vps = &myrequest->coa->proxy_reply->vps;
+               }
+#endif
 
        } else {
                return RLM_MODULE_INVALID;
index 5e7ff97..d620dea 100644 (file)
@@ -180,6 +180,7 @@ static REQUEST *lookup_in_proxy_hash(RADIUS_PACKET *reply)
         *      responded at all.
         */
        if (!request->proxy_reply &&
+           request->home_server &&
            request->home_server->currently_outstanding) {
                request->home_server->currently_outstanding--;
        }
@@ -204,6 +205,7 @@ static void remove_from_proxy_hash(REQUEST *request)
         *      home server.
         */
        if (!request->proxy_reply &&
+           request->home_server &&
            request->home_server->currently_outstanding) {
                request->home_server->currently_outstanding--;
        }
@@ -213,8 +215,64 @@ static void remove_from_proxy_hash(REQUEST *request)
        request->in_proxy_hash = FALSE;
 }
 
+static int proxy_id_alloc(REQUEST *request, RADIUS_PACKET *packet)
+{
+       int i, proxy, found;
+       rad_listen_t *proxy_listener;
+
+       if (fr_packet_list_id_alloc(proxy_list, packet)) return 1;
 
-static int insert_into_proxy_hash(REQUEST *request)
+       /*
+        *      Allocate a new proxy fd.  This function adds
+        *      it to the tail of the list of listeners.  With
+        *      some care, this can be thread-safe.
+        */
+       proxy_listener = proxy_new_listener();
+       if (!proxy_listener) {
+               RDEBUG2("ERROR: Failed to create a new socket for proxying requests.");
+               return 0;
+       }
+       
+       /*
+        *      Cache it locally.
+        */
+       found = -1;
+       proxy = proxy_listener->fd;
+       for (i = 0; i < 32; i++) {
+               /*
+                *      Found a free entry.  Save the socket,
+                *      and remember where we saved it.
+                */
+               if (proxy_fds[(proxy + i) & 0x1f] == -1) {
+                       found = (proxy + i) & 0x1f;
+                       proxy_fds[found] = proxy;
+                       proxy_listeners[found] = proxy_listener;
+                       break;
+               }
+       }
+       rad_assert(found >= 0);
+       
+       if (!fr_packet_list_socket_add(proxy_list, proxy_listener->fd)) {
+                       RDEBUG2("ERROR: Failed to create a new socket for proxying requests.");
+               return 0;
+               
+       }
+       
+       if (!fr_packet_list_id_alloc(proxy_list, packet)) {
+                       RDEBUG2("ERROR: Failed to create a new socket for proxying requests.");
+               return 0;
+       }
+       
+       /*
+        *      Signal the main thread to add the new FD to the list
+        *      of listening FD's.
+        */
+       radius_signal_self(RADIUS_SIGNAL_SELF_NEW_FD);
+       return 1;
+}
+
+
+static int insert_into_proxy_hash(REQUEST *request, int retransmit)
 {
        int i, proxy;
        char buf[128];
@@ -222,66 +280,41 @@ static int insert_into_proxy_hash(REQUEST *request)
        rad_assert(request->proxy != NULL);
        rad_assert(proxy_list != NULL);
 
-       request->proxy->sockfd = -1;
-
        PTHREAD_MUTEX_LOCK(&proxy_mutex);
 
-       request->home_server->currently_outstanding++;
-
-       if (!fr_packet_list_id_alloc(proxy_list, request->proxy)) {
-               int found;
-               rad_listen_t *proxy_listener;
-
-               /*
-                *      Allocate a new proxy fd.  This function adds
-                *      it to the tail of the list of listeners.  With
-                *      some care, this can be thread-safe.
-                */
-               proxy_listener = proxy_new_listener();
-               if (!proxy_listener) {
-                       PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
-                       RDEBUG2("ERROR: Failed to create a new socket for proxying requests.");
-                       return 0;
-               }
-
-               /*
-                *      Cache it locally.
-                */
-               found = -1;
-               proxy = proxy_listener->fd;
-               for (i = 0; i < 32; i++) {
-                       /*
-                        *      Found a free entry.  Save the socket,
-                        *      and remember where we saved it.
-                        */
-                       if (proxy_fds[(proxy + i) & 0x1f] == -1) {
-                               found = (proxy + i) & 0x1f;
-                               proxy_fds[found] = proxy;
-                               proxy_listeners[found] = proxy_listener;
-                               break;
-                       }
-               }
-               rad_assert(found >= 0);
+       /*
+        *      Keep track of maximum outstanding requests to a
+        *      particular home server.  'max_outstanding' is
+        *      enforced in home_server_ldb(), in realms.c.
+        */
+       if (request->home_server) {
+               request->home_server->currently_outstanding++;
+               request->home_server->stats.total_requests++;
+       }
 
-               if (!fr_packet_list_socket_add(proxy_list, proxy_listener->fd)) {
-                       PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
-                       RDEBUG2("ERROR: Failed to create a new socket for proxying requests.");
-                       return 0;
+       if (retransmit) {
+               RADIUS_PACKET packet;
 
-               }
+               packet = *request->proxy;
 
-               if (!fr_packet_list_id_alloc(proxy_list, request->proxy)) {
+               if (!proxy_id_alloc(request, &packet)) {
                        PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
-                       RDEBUG2("ERROR: Failed to create a new socket for proxying requests.");
                        return 0;
                }
 
                /*
-                *      Signal the main thread to add the new FD to the list
-                *      of listening FD's.
+                *      Yank the request, free the old Id, and
+                *      remember the new Id.
                 */
-               radius_signal_self(RADIUS_SIGNAL_SELF_NEW_FD);
+               fr_packet_list_yank(proxy_list, request->proxy);
+               fr_packet_list_id_free(proxy_list, request->proxy);
+               *request->proxy = packet;
+
+       } else if (!proxy_id_alloc(request, request->proxy)) {
+               PTHREAD_MUTEX_UNLOCK(&proxy_mutex);
+               return 0;
        }
+
        rad_assert(request->proxy->sockfd >= 0);
 
        /*
@@ -333,14 +366,20 @@ static int insert_into_proxy_hash(REQUEST *request)
 static void wait_for_proxy_id_to_expire(void *ctx)
 {
        REQUEST *request = ctx;
-       home_server *home = request->home_server;
 
        rad_assert(request->magic == REQUEST_MAGIC);
        rad_assert(request->proxy != NULL);
 
        if (!fr_event_now(el, &now)) gettimeofday(&now, NULL);
        request->when = request->proxy_when;
-       request->when.tv_sec += home->response_window;
+
+#ifdef WITH_COA
+       if ((request->packet->code == PW_COA_REQUEST) ||
+           (request->packet->code == PW_DISCONNECT_REQUEST)) {
+               request->when.tv_sec += request->home_server->coa_mrd;
+       } else
+#endif
+       request->when.tv_sec += request->home_server->response_window;
 
        if ((request->num_proxied_requests == request->num_proxied_responses) ||
            timercmp(&now, &request->when, >)) {
@@ -353,6 +392,7 @@ static void wait_for_proxy_id_to_expire(void *ctx)
                               request->number,
                               (unsigned int) (request->timestamp - fr_start_time));
                }
+
                fr_event_delete(el, &request->ev);
                remove_from_proxy_hash(request);
                remove_from_request_hash(request);
@@ -581,9 +621,12 @@ void revive_home_server(void *ctx)
 static void no_response_to_ping(void *ctx)
 {
        REQUEST *request = ctx;
-       home_server *home = request->home_server;
+       home_server *home;
        char buffer[128];
 
+       rad_assert(request->home_server != NULL);
+
+       home = request->home_server;
        home->num_received_pings = 0;
 
        RDEBUG2("No response to status check %d from home server %s port %d",
@@ -599,9 +642,12 @@ static void no_response_to_ping(void *ctx)
 
 static void received_response_to_ping(REQUEST *request)
 {
-       home_server *home = request->home_server;
+       home_server *home;
        char buffer[128];
 
+       rad_assert(request->home_server != NULL);
+
+       home = request->home_server;
        home->num_received_pings++;
 
        RDEBUG2("Received response to status check %d (%d in current sequence)",
@@ -718,7 +764,7 @@ static void ping_home_server(void *ctx)
 
        rad_assert(request->proxy_listener == NULL);
 
-       if (!insert_into_proxy_hash(request)) {
+       if (!insert_into_proxy_hash(request, FALSE)) {
                RDEBUG2("ERROR: Failed inserting status check %d into proxy hash.  Discarding it.",
                       request->number);
                request_free(&request);
@@ -860,6 +906,23 @@ static int setup_post_proxy_fail(REQUEST *request)
        } else if (request->packet->code == PW_ACCOUNTING_REQUEST) {
                dval = dict_valbyname(PW_POST_PROXY_TYPE, "Fail-Accounting");
 
+#ifdef WITH_COA
+               /*
+                *      See no_response_to_coa_request
+                */
+       } else if (((request->packet->code >> 8) & 0xff) == PW_COA_REQUEST) {
+               request->packet->code &= 0xff; /* restore it */
+
+               if (request->proxy->code == PW_COA_REQUEST) {
+                       dval = dict_valbyname(PW_POST_PROXY_TYPE, "Fail-CoA");
+
+               } else if (request->proxy->code == PW_DISCONNECT_REQUEST) {
+                       dval = dict_valbyname(PW_POST_PROXY_TYPE, "Fail-Disconnect");
+               } else {
+                       return 0;
+               }
+
+#endif
        } else {
                return 0;
        }
@@ -1011,6 +1074,23 @@ static void wait_a_bit(void *ctx)
 
        rad_assert(request->magic == REQUEST_MAGIC);
 
+#ifdef WITH_COA
+       /*
+        *      The CoA request is a new (internally generated)
+        *      request, created in a child thread.  We therefore need
+        *      some way to tie its events back into the main event
+        *      handler.
+        */
+       if (request->coa && !request->coa->proxy_reply &&
+           request->coa->next_callback) {
+               request->coa->when = request->coa->next_when;
+               INSERT_EVENT(request->coa->next_callback, request->coa);
+               request->coa->next_callback = NULL;
+               request->coa->parent = NULL;
+               request->coa = NULL;
+       }
+#endif
+
        switch (request->child_state) {
        case REQUEST_QUEUED:
        case REQUEST_RUNNING:
@@ -1107,6 +1187,25 @@ static void wait_a_bit(void *ctx)
 #ifdef HAVE_PTHREAD_H
                request->child_pid = NO_SUCH_CHILD_PID;
 #endif
+
+#ifdef WTH_COA
+               /*
+                *      This is a CoA request.  It's been divorced
+                *      from everything else, so we clean it up now.
+                */
+               if (!request->in_request_hash &&
+                   request->proxy &&
+                   ((request->proxy->code == PW_COA_REQUEST) ||
+                    (request->proxy->code == PW_DISCONNECT_REQUEST))) {
+                       /*
+                        *      FIXME: Do CoA MIBs
+                        */
+                       fr_event_delete(el, &request->ev);
+                       remove_from_proxy_hash(request);
+                       request_free(&request);
+                       return;
+               }
+#endif
                request_stats_final(request);
                cleanup_delay(request);
                return;
@@ -1154,6 +1253,369 @@ static void wait_a_bit(void *ctx)
        INSERT_EVENT(callback, request);
 }
 
+#ifdef WITH_COA
+static void no_response_to_coa_request(void *ctx)
+{
+       REQUEST *request = ctx;
+       char buffer[128];
+
+       rad_assert(request->magic == REQUEST_MAGIC);
+       rad_assert(request->child_state == REQUEST_PROXIED);
+       rad_assert(request->home_server != NULL);
+       rad_assert(!request->in_request_hash);
+
+       radlog(L_ERR, "No response to CoA request sent to %s",
+              inet_ntop(request->proxy->dst_ipaddr.af,
+                        &request->proxy->dst_ipaddr.ipaddr,
+                        buffer, sizeof(buffer)));
+
+       /*
+        *      Hack.
+        */
+       request->packet->code |= (PW_COA_REQUEST << 8);
+       post_proxy_fail_handler(request);
+}
+
+
+static int update_event_timestamp(RADIUS_PACKET *packet, time_t when)
+{
+       VALUE_PAIR *vp;
+
+       vp = pairfind(packet->vps, PW_EVENT_TIMESTAMP);
+       if (!vp) return 0;
+
+       vp->vp_date = when;
+
+       if (packet->data) {
+               free(packet->data);
+               packet->data = NULL;
+               packet->data_len = 0;
+       }
+
+       return 1;               /* time stamp updated */
+}
+
+
+/*
+ *     Called when we haven't received a response to a CoA request.
+ */
+static void retransmit_coa_request(void *ctx)
+{
+       int delay, frac;
+       struct timeval mrd;
+       REQUEST *request = ctx;
+
+       rad_assert(request->magic == REQUEST_MAGIC);
+       rad_assert(request->child_state == REQUEST_PROXIED);
+       rad_assert(request->home_server != NULL);
+       rad_assert(!request->in_request_hash);
+       rad_assert(request->parent == NULL);
+       
+       fr_event_now(el, &now);
+
+       /*
+        *      Cap count at MRC, if it is non-zero.
+        */
+       if (request->home_server->coa_mrc &&
+           (request->num_coa_requests >= request->home_server->coa_mrc)) {
+               no_response_to_coa_request(request);
+               return;
+       }
+
+       /*
+        *      RFC 5080 Section 2.2.1
+        *
+        *      RT = 2*RTprev + RAND*RTprev
+        *         = 1.9 * RTprev + rand(0,.2) * RTprev
+        *         = 1.9 * RTprev + rand(0,1) * (RTprev / 5)
+        */
+       delay = fr_rand();
+       delay ^= (delay >> 16);
+       delay &= 0xffff;
+       frac = request->delay / 5;
+       delay = ((frac >> 16) * delay) + (((frac & 0xffff) * delay) >> 16);
+
+       delay += (2 * request->delay) - (request->delay / 10);
+
+       /*
+        *      Cap delay at MRT, if MRT is non-zero.
+        */
+       if (request->home_server->coa_mrt &&
+           (delay > (request->home_server->coa_mrt * USEC))) {
+               int mrt_usec = request->home_server->coa_mrt * USEC;
+
+               /*
+                *      delay = MRT + RAND * MRT
+                *            = 0.9 MRT + rand(0,.2)  * MRT
+                */
+               delay = fr_rand();
+               delay ^= (delay >> 15);
+               delay &= 0x1ffff;
+               delay = ((mrt_usec >> 16) * delay) + (((mrt_usec & 0xffff) * delay) >> 16);
+               delay += mrt_usec - (mrt_usec / 10);
+       }
+
+       request->delay = delay;
+       request->when = now;
+       tv_add(&request->when, request->delay);
+       mrd = request->proxy_when;
+       mrd.tv_sec += request->home_server->coa_mrd;
+
+       /*
+        *      Cap duration at MRD.
+        */
+       if (timercmp(&mrd, &request->when, <)) {
+               request->when = mrd;
+               INSERT_EVENT(no_response_to_coa_request, request);
+
+       } else {
+               INSERT_EVENT(retransmit_coa_request, request);
+       }
+       
+       if (update_event_timestamp(request->proxy, now.tv_sec)) {
+               if (!insert_into_proxy_hash(request, TRUE)) {
+                       DEBUG("ERROR: Failed re-inserting CoA request into proxy hash.");
+                       return;
+               }
+
+               request->num_proxied_requests = 0;
+               request->num_proxied_responses = 0;
+       }
+
+       request->num_proxied_requests++;
+       request->num_coa_requests++; /* is NOT reset by code 3 lines above! */
+
+       request->proxy_listener->send(request->proxy_listener,
+                                     request);
+}
+
+
+/*
+ *     The original request is either DONE, or in CLEANUP_DELAY.
+ */
+static int originated_coa_request(REQUEST *request)
+{
+       int delay, rcode, pre_proxy_type = 0;
+       VALUE_PAIR *vp;
+       REQUEST *coa;
+       fr_ipaddr_t ipaddr;
+       char buffer[256];
+
+       rad_assert(request->proxy == NULL);
+       rad_assert(!request->in_proxy_hash);
+       rad_assert(request->proxy_reply == NULL);
+
+       vp = pairfind(request->config_items, PW_SEND_COA_REQUEST);
+       if (!vp && request->coa) vp = pairfind(request->coa->proxy->vps, PW_SEND_COA_REQUEST);
+       if (vp) {
+               if (vp->vp_integer == 0) {
+                       request_free(&request->coa);
+                       return 1;       /* success */
+               }
+
+               if (!request->coa) request_alloc_coa(request);
+               if (!request->coa) return 0;
+       }
+
+       coa = request->coa;
+
+       /*
+        *      src_ipaddr will be set up in proxy_encode.
+        */
+       memset(&ipaddr, 0, sizeof(ipaddr));
+       vp = pairfind(coa->proxy->vps, PW_PACKET_DST_IP_ADDRESS);
+       if (vp) {
+               ipaddr.af = AF_INET;
+               ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr;
+
+       } else if ((vp = pairfind(coa->proxy->vps,
+                                 PW_PACKET_DST_IPV6_ADDRESS)) != NULL) {
+               ipaddr.af = AF_INET6;
+               ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr;
+               
+       } else if ((vp = pairfind(coa->proxy->vps,
+                                 PW_HOME_SERVER_POOL)) != NULL) {
+               coa->home_pool = home_pool_byname(vp->vp_strvalue,
+                                                 HOME_TYPE_COA);
+               if (!coa->home_pool) {
+                       RDEBUG2("WARNING: No such home_server_pool %s",
+                              vp->vp_strvalue);
+       fail:
+                       request_free(&request->coa);
+                       return 0;
+               }
+
+               /*
+                *      Prefer
+                */
+       } else if (request->client->coa_pool) {
+               coa->home_pool = request->client->coa_pool;
+
+       } else if (request->client->coa_server) {
+               coa->home_server = request->client->coa_server;
+
+       } else {
+               /*
+                *      If all else fails, send it to the client that
+                *      originated this request.
+                */
+               memcpy(&ipaddr, &request->packet->src_ipaddr, sizeof(ipaddr));
+       }
+
+       /*
+        *      Use the pool, if it exists.
+        */
+       if (coa->home_pool) {
+               coa->home_server = home_server_ldb(NULL, coa->home_pool, coa);
+               if (!coa->home_server) {
+                       RDEBUG("WARNING: No live home server for home_server_pool %s", vp->vp_strvalue);
+                       goto fail;
+               }
+
+       } else if (!coa->home_server) {
+               int port = PW_COA_UDP_PORT;
+
+               vp = pairfind(coa->proxy->vps, PW_PACKET_DST_PORT);
+               if (vp) port = vp->vp_integer;
+
+               coa->home_server = home_server_find(&ipaddr, port);
+               if (!coa->home_server) {
+                       RDEBUG2("WARNING: Unknown destination %s:%d for CoA request.",
+                              inet_ntop(ipaddr.af, &ipaddr.ipaddr,
+                                        buffer, sizeof(buffer)), port);
+                       goto fail;
+               }
+       }
+
+       vp = pairfind(coa->proxy->vps, PW_PACKET_TYPE);
+       if (vp) {
+               switch (vp->vp_integer) {
+               case PW_COA_REQUEST:
+               case PW_DISCONNECT_REQUEST:
+                       coa->proxy->code = vp->vp_integer;
+                       break;
+                       
+               default:
+                       DEBUG("Cannot set CoA Packet-Type to code %d",
+                             vp->vp_integer);
+                       goto fail;
+               }
+       }
+
+       /*
+        *      The rest of the server code assumes that
+        *      request->packet && request->reply exist.  Copy them
+        *      from the original request.
+        */
+       rad_assert(coa->packet != NULL);
+       rad_assert(coa->packet->vps == NULL);
+       memcpy(coa->packet, request->packet, sizeof(*request->packet));
+       coa->packet->vps = paircopy(request->packet->vps);
+       coa->packet->data = NULL;
+       rad_assert(coa->reply != NULL);
+       rad_assert(coa->reply->vps == NULL);
+       memcpy(coa->reply, request->reply, sizeof(*request->reply));
+       coa->reply->vps = paircopy(request->reply->vps);
+       coa->reply->data = NULL;
+       coa->config_items = paircopy(request->config_items);
+
+       /*
+        *      Call the pre-proxy routines.
+        */
+       vp = pairfind(request->config_items, PW_PRE_PROXY_TYPE);
+       if (vp) {
+               RDEBUG2("  Found Pre-Proxy-Type %s", vp->vp_strvalue);
+               pre_proxy_type = vp->vp_integer;
+       }
+
+       if (coa->home_server && coa->home_pool->virtual_server) {
+               const char *old_server = coa->server;
+               
+               coa->server = coa->home_pool->virtual_server;
+               RDEBUG2(" server %s {", coa->server);
+               rcode = module_pre_proxy(pre_proxy_type, coa);
+               RDEBUG2(" }");
+               coa->server = old_server;
+       } else {
+               rcode = module_pre_proxy(pre_proxy_type, coa);
+       }
+       switch (rcode) {
+       default:
+               goto fail;
+
+       /*
+        *      Only send the CoA packet if the pre-proxy code succeeded.
+        */
+       case RLM_MODULE_NOOP:
+       case RLM_MODULE_OK:
+       case RLM_MODULE_UPDATED:
+               break;
+       }
+
+       /*
+        *      Source IP / port is set when the proxy socket
+        *      is chosen.
+        */
+       coa->proxy->dst_ipaddr = coa->home_server->ipaddr;
+       coa->proxy->dst_port = coa->home_server->port;
+
+       if (!insert_into_proxy_hash(coa, FALSE)) {
+               DEBUG("ERROR: Failed inserting CoA request into proxy hash.");
+               goto fail;
+       }
+
+       /*
+        *      Forget about the original request completely at this
+        *      point.
+        */
+       request = coa;
+
+       gettimeofday(&request->proxy_when, NULL);       
+       request->received = request->next_when = request->proxy_when;
+       rad_assert(request->proxy_reply == NULL);
+
+       /*
+        *      Implement re-transmit algorithm as per RFC 5080
+        *      Section 2.2.1.
+        *
+        *      We want IRT + RAND*IRT
+        *      or 0.9 IRT + rand(0,.2) IRT
+        *
+        *      2^20 ~ USEC, and we want 2.
+        *      rand(0,0.2) USEC ~ (rand(0,2^21) / 10)
+        */
+       delay = (fr_rand() & ((1 << 22) - 1)) / 10;
+       request->delay = delay * request->home_server->coa_irt;
+       delay = request->home_server->coa_irt * USEC;
+       delay -= delay / 10;
+       delay += request->delay;
+     
+       request->delay = delay;
+       tv_add(&request->next_when, delay);
+       request->next_callback = retransmit_coa_request;
+       
+       /*
+        *      Note that we set proxied BEFORE sending the packet.
+        *
+        *      Once we send it, the request is tainted, as
+        *      another thread may have picked it up.  Don't
+        *      touch it!
+        */
+       request->num_proxied_requests = 1;
+       request->num_proxied_responses = 0;
+       request->child_pid = NO_SUCH_CHILD_PID;
+
+       update_event_timestamp(request->proxy, request->proxy_when.tv_sec);
+
+       request->child_state = REQUEST_PROXIED;
+
+       DEBUG_PACKET(request, request->proxy, 1);
+
+       request->proxy_listener->send(request->proxy_listener,
+                                     request);
+       return 1;
+}
+#endif /* WITH_COA */
 
 #ifdef WITH_PROXY
 static int process_proxy_reply(REQUEST *request)
@@ -1190,6 +1652,12 @@ static int process_proxy_reply(REQUEST *request)
        } else {
                rcode = module_post_proxy(post_proxy_type, request);
        }
+
+#ifdef WITH_COA
+       if ((request->proxy->code != PW_COA_REQUEST) &&
+           (request->proxy->code != PW_DISCONNECT_REQUEST))
+               /* quietly follow the next if request->proxy_reply */
+#endif
        
        /*
         *      There may NOT be a proxy reply, as we may be
@@ -1296,15 +1764,13 @@ static int request_pre_handler(REQUEST *request)
                return 0;
        }
 
-#ifdef WITH_PROXY
-       if (!request->proxy)
-#endif
-         {
-                 request->username = pairfind(request->packet->vps,
+       if (!request->username) {
+               request->username = pairfind(request->packet->vps,
                                             PW_USER_NAME);
+       }
 
 #ifdef WITH_PROXY
-       } else {
+       if (request->proxy) {
                return process_proxy_reply(request);
 #endif
        }
@@ -1327,7 +1793,7 @@ static int proxy_request(REQUEST *request)
                return 0;
        }
 
-       if (!insert_into_proxy_hash(request)) {
+       if (!insert_into_proxy_hash(request, FALSE)) {
                RDEBUG("ERROR: Failed inserting request into proxy hash.");
                return 0;
        }
@@ -1431,7 +1897,6 @@ static int proxy_to_virtual_server(REQUEST *request)
        return 2;               /* success, but NOT '1' !*/
 }
 
-
 /*
  *     Return 1 if we did proxy it, or the proxy attempt failed
  *     completely.  Either way, the caller doesn't touch the request
@@ -1444,7 +1909,7 @@ static int successfully_proxied_request(REQUEST *request)
        VALUE_PAIR *realmpair;
        VALUE_PAIR *strippedname;
        VALUE_PAIR *vp;
-       char *realmname;
+       char *realmname = NULL;
        home_server *home;
        REALM *realm = NULL;
        home_pool_t *pool;
@@ -1694,7 +2159,6 @@ static int successfully_proxied_request(REQUEST *request)
 }
 #endif
 
-
 static void request_post_handler(REQUEST *request)
 {
        int child_state = -1;
@@ -1708,14 +2172,29 @@ static void request_post_handler(REQUEST *request)
 #ifdef HAVE_PTHREAD_H
                request->child_pid = NO_SUCH_CHILD_PID;
 #endif
-               request->child_state = REQUEST_DONE;
-               return;
+               child_state = REQUEST_DONE;
+               goto cleanup;
        }
 
        if (request->child_state != REQUEST_RUNNING) {
                rad_panic("Internal sanity check failed");
        }
 
+#ifdef WITH_COA
+       /*
+        *      If it's not in the request hash, it's a CoA request.
+        *      We hope.
+        */
+       if (!request->in_request_hash &&
+           request->proxy &&
+           ((request->proxy->code == PW_COA_REQUEST) ||
+            (request->proxy->code == PW_DISCONNECT_REQUEST))) {
+               request->next_callback = NULL;
+               child_state = REQUEST_DONE;
+               goto cleanup;
+       }
+#endif
+
        if ((request->reply->code == 0) &&
            ((vp = pairfind(request->config_items, PW_AUTH_TYPE)) != NULL) &&
            (vp->vp_integer == PW_AUTHTYPE_REJECT)) {
@@ -1730,6 +2209,14 @@ static void request_post_handler(REQUEST *request)
            (request->packet->code != PW_STATUS_SERVER)) {
                int rcode = successfully_proxied_request(request);
 
+#ifdef WITH_COA
+               /*
+                *      If we proxy it, we CANNOT originate a CoA
+                *      request at the same time.
+                */
+               if (rcode != 0) request_free(&request->coa);
+#endif
+
                if (rcode == 1) return;
 
                /*
@@ -1866,7 +2353,8 @@ static void request_post_handler(REQUEST *request)
                }
 
                radlog(L_ERR, "Unknown packet type %d", request->packet->code);
-               rad_panic("Unknown packet type");
+               request->next_callback = NULL;
+               child_state = REQUEST_DONE;
                break;
        }
 
@@ -1883,6 +2371,21 @@ static void request_post_handler(REQUEST *request)
                request->listener->send(request->listener, request);
        }
 
+#ifdef WITH_COA
+       /*
+        *      Now that we've completely processed the request,
+        *      see if we need to originate a CoA request.
+        */
+       if (request->coa ||
+           (pairfind(request->config_items, PW_SEND_COA_REQUEST) != NULL)) {
+               if (!originated_coa_request(request)) {
+                       RDEBUG2("Do CoA Fail handler here");
+               }
+               /* request->coa is stil set, so we can update events */
+       }
+#endif
+
+ cleanup:
        /*
         *      Clean up.  These are no longer needed.
         */
@@ -1918,7 +2421,7 @@ static void request_post_handler(REQUEST *request)
 #endif
 
        RDEBUG2("Finished request %d.", request->number);
-
+       rad_assert(child_state >= 0);
        request->child_state = child_state;
 
        /*
@@ -1936,7 +2439,7 @@ static void received_retransmit(REQUEST *request, const RADCLIENT *client)
 
        RAD_STATS_TYPE_INC(request->listener, total_dup_requests);
        RAD_STATS_CLIENT_INC(request->listener, client, total_dup_requests);
-
+       
        switch (request->child_state) {
        case REQUEST_QUEUED:
        case REQUEST_RUNNING:
@@ -1963,9 +2466,15 @@ static void received_retransmit(REQUEST *request, const RADCLIENT *client)
                 *
                 *      Instead, we just discard the packet.  We may
                 *      eventually respond, or the client will send a
-                *      new accounting packet.
+                *      new accounting packet.            
+                *
+                *      The same comments go for Status-Server, and
+                *      other packet types.
+                *
+                *      FIXME: coa: when we proxy CoA && Disconnect
+                *      packets, this logic has to be fixed.
                 */
-               if (request->packet->code == PW_ACCOUNTING_REQUEST) {
+               if (request->packet->code != PW_AUTHENTICATION_REQUEST) {
                        goto discard;
                }
 
@@ -2057,6 +2566,13 @@ static void received_retransmit(REQUEST *request, const RADCLIENT *client)
 
        case REQUEST_CLEANUP_DELAY:
        case REQUEST_DONE:
+               /*
+                *      FIXME: This sends duplicate replies to
+                *      accounting requests, even if Acct-Delay-Time
+                *      or Event-Timestamp is in the packet.  In those
+                *      cases, the Id should be changed, and the packet
+                *      re-calculated.
+                */
                RDEBUG2("Sending duplicate reply "
                       "to client %s port %d - ID: %d",
                       client->shortname,
@@ -2329,58 +2845,69 @@ int received_request(rad_listen_t *listener,
 REQUEST *received_proxy_response(RADIUS_PACKET *packet)
 {
        char            buffer[128];
-       home_server     *home;
        REQUEST         *request;
 
-       if (!home_server_find(&packet->src_ipaddr, packet->src_port)) {
-               radlog(L_ERR, "Ignoring request from unknown home server %s port %d",
-                      inet_ntop(packet->src_ipaddr.af,
-                                &packet->src_ipaddr.ipaddr,
-                                buffer, sizeof(buffer)),
-                              packet->src_port);
-               rad_free(&packet);
-               return NULL;
-       }
-
        /*
         *      Also removes from the proxy hash if responses == requests
         */
        request = lookup_in_proxy_hash(packet);
 
        if (!request) {
-               radlog(L_PROXY, "No outstanding request was found for proxy reply from home server %s port %d - ID %d",
+               radlog(L_PROXY, "No outstanding request was found for reply from host %s port %d - ID %d",
                       inet_ntop(packet->src_ipaddr.af,
                                 &packet->src_ipaddr.ipaddr,
                                 buffer, sizeof(buffer)),
                       packet->src_port, packet->id);
-               rad_free(&packet);
                return NULL;
        }
 
-       home = request->home_server;
-
        gettimeofday(&now, NULL);
 
+       request->home_server->state = HOME_STATE_ALIVE;
+       
+#ifdef WITH_COA
        /*
-        *      FIXME: mark the home server alive?
+        *      This is a response to a CoA packet that we originated.
+        *      It's handled differently from normal proxied packets.
         */
-       home->state = HOME_STATE_ALIVE;
+       if ((request->proxy->code == PW_COA_REQUEST) ||
+           (request->proxy->code == PW_DISCONNECT_REQUEST)) {
+               /*
+                *      The parent request is done, but we haven't
+                *      figured that out yet.  Separate the two
+                *      requests here, the FIRST time we process the
+                *      packet.  If there is a proxy reply already, it
+                *      gets ignored below.
+                */
+               if (!request->proxy_reply && request->parent &&
+                   (request->parent->coa == request)) {
+                       request->parent->coa = NULL;
+                       request->parent = NULL;
+               }
+
+               /*
+                *      request->reply exists, and we don't care about
+                *      it here.  So we skip the next step.
+                */
+               rad_assert(request->packet != NULL);
+               rad_assert(request->reply != NULL);
+       } else
+#endif
 
        if (request->reply && request->reply->code != 0) {
-               RDEBUG2("We already replied to this request.  Discarding response from home server.");
-               rad_free(&packet);
+               RDEBUG2("We already replied to this request.  Discarding response.");
                return NULL;
        }
-
+       
        /*
-        *      We had previously received a reply, so we don't need
-        *      to do anything here.
+        *      We had previously received a reply, so we
+        *      don't need to do anything here.
         */
        if (request->proxy_reply) {
                if (memcmp(request->proxy_reply->vector,
                           packet->vector,
                           sizeof(request->proxy_reply->vector)) == 0) {
-                       RDEBUG2("Discarding duplicate reply from home server %s port %d  - ID: %d for request %d",
+                       RDEBUG2("Discarding duplicate reply from host %s port %d  - ID: %d for request %d",
                               inet_ntop(packet->src_ipaddr.af,
                                         &packet->src_ipaddr.ipaddr,
                                         buffer, sizeof(buffer)),
@@ -2394,9 +2921,8 @@ REQUEST *received_proxy_response(RADIUS_PACKET *packet)
                         */
                        RDEBUG2("Ignoring conflicting proxy reply");
                }
-
+               
                /* assert that there's an event queued for request? */
-               rad_free(&packet);
                return NULL;
        }
 #ifdef WITH_STATS
@@ -2408,17 +2934,17 @@ REQUEST *received_proxy_response(RADIUS_PACKET *packet)
         *      We update the response time only for the FIRST packet
         *      we receive.
         */
-       else if (home->ema.window > 0) {
-               radius_stats_ema(&home->ema, &now, &request->proxy_when);
+       else if (request->home_server->ema.window > 0) {
+               radius_stats_ema(&request->home_server->ema,
+                                &now, &request->proxy_when);
        }
 #endif
 
-
        switch (request->child_state) {
        case REQUEST_QUEUED:
        case REQUEST_RUNNING:
-               rad_panic("Internal sanity check failed for child state");
-               break;
+               radlog(L_ERR, "Internal sanity check failed for child state");
+               /* FALL-THROUGH */
 
        case REQUEST_REJECT_DELAY:
        case REQUEST_CLEANUP_DELAY:
@@ -2430,7 +2956,6 @@ REQUEST *received_proxy_response(RADIUS_PACKET *packet)
                       packet->src_port, packet->id,
                       request->number);
                /* assert that there's an event queued for request? */
-               rad_free(&packet);
                return NULL;
 
        case REQUEST_PROXIED:
index 3ee1ee3..7806105 100644 (file)
@@ -912,6 +912,39 @@ static int acct_socket_recv(rad_listen_t *listener,
 }
 #endif
 
+
+#ifdef WITH_COA
+/*
+ *     For now, all CoA requests are *only* originated, and not
+ *     proxied.  So all of the necessary work is done in the
+ *     post-proxy section, which is automatically handled by event.c.
+ *     As a result, we don't have to do anything here.
+ */
+static int rad_coa_reply(REQUEST *request)
+{
+       VALUE_PAIR *s1, *s2;
+
+       /*
+        *      Inform the user about RFC requirements.
+        */
+       s1 = pairfind(request->proxy->vps, PW_STATE);
+       if (s1) {
+               s2 = pairfind(request->proxy_reply->vps, PW_STATE);
+
+               if (!s2) {
+                       DEBUG("WARNING: Client was sent State in CoA, and did not respond with State.");
+
+               } else if ((s1->length != s2->length) ||
+                          (memcmp(s1->vp_octets, s2->vp_octets,
+                                  s1->length) != 0)) {
+                       DEBUG("WARNING: Client was sent State in CoA, and did not respond with the same State.");
+               }
+       }
+
+       return RLM_MODULE_OK;
+}
+#endif
+
 #ifdef WITH_PROXY
 /*
  *     Recieve packets from a proxy socket.
@@ -946,6 +979,15 @@ static int proxy_socket_recv(rad_listen_t *listener,
                break;
 #endif
 
+#ifdef WITH_COA
+       case PW_DISCONNECT_ACK:
+       case PW_DISCONNECT_NAK:
+       case PW_COA_ACK:
+       case PW_COA_NAK:
+               fun = rad_coa_reply;
+               break;
+#endif
+
        default:
                /*
                 *      FIXME: Update MIB for packet types?
@@ -961,6 +1003,7 @@ static int proxy_socket_recv(rad_listen_t *listener,
 
        request = received_proxy_response(packet);
        if (!request) {
+               rad_free(&packet);
                return 0;
        }
 
index 754a23f..970e773 100644 (file)
@@ -847,13 +847,13 @@ int read_mainconfig(int reload)
        cf_section_free(&mainconfig.config);
        mainconfig.config = cs;
 
-       if (!clients_parse_section(cs)) {
+       DEBUG2("%s: #### Loading Realms and Home Servers ####", mainconfig.name);
+       if (!realms_init(cs)) {
                return -1;
        }
 
-       DEBUG2("%s: #### Loading Realms and Home Servers ####", mainconfig.name);
-
-       if (!realms_init(cs)) {
+       DEBUG2("%s: #### Loading Clients ####", mainconfig.name);
+       if (!clients_parse_section(cs)) {
                return -1;
        }
 
index 4447749..d7d9c31 100644 (file)
@@ -1165,6 +1165,7 @@ static modcallable *do_compile_modupdate(modcallable *parent,
                                         const char *name2)
 {
        int i, ok = FALSE;
+       const char *vp_name;
        modgroup *g;
        modcallable *csingle;
        CONF_ITEM *ci;
@@ -1173,8 +1174,7 @@ static modcallable *do_compile_modupdate(modcallable *parent,
        static const char *attrlist_names[] = {
                "request", "reply", "proxy-request", "proxy-reply",
                "config", "control",
-               "outer.request", "outer.reply",
-               "outer.config", "outer.control",
+               "coa", "coa-reply", "disconnect", "disconnect-reply",
                NULL
        };
 
@@ -1186,8 +1186,13 @@ static modcallable *do_compile_modupdate(modcallable *parent,
                return NULL;
        }
 
+       vp_name = name2;
+       if (strncmp(vp_name, "outer.", 6) == 0) {
+               vp_name += 6;
+       } 
+
        for (i = 0; attrlist_names[i] != NULL; i++) {
-               if (strcmp(name2, attrlist_names[i]) == 0) {
+               if (strcmp(vp_name, attrlist_names[i]) == 0) {
                        ok = TRUE;
                        break;
                }
index fde36f9..42aec94 100644 (file)
@@ -330,12 +330,22 @@ static CONF_PARSER home_server_config[] = {
          offsetof(home_server,ema.window), NULL,  NULL },
 #endif
 
-       { NULL, -1, 0, NULL, NULL }             /* end the list */
+#ifdef WITH_COA
+       { "irt",  PW_TYPE_INTEGER,
+         offsetof(home_server, coa_irt), 0, Stringify(2) },
+       { "mrt",  PW_TYPE_INTEGER,
+         offsetof(home_server, coa_mrt), 0, Stringify(16) },
+       { "mrc",  PW_TYPE_INTEGER,
+         offsetof(home_server, coa_mrc), 0, Stringify(5) },
+       { "mrd",  PW_TYPE_INTEGER,
+         offsetof(home_server, coa_mrd), 0, Stringify(30) },
+#endif
 
+       { NULL, -1, 0, NULL, NULL }             /* end the list */
 };
 
 
-static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
+static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int pool_type)
 {
        const char *name2;
        home_server *home;
@@ -367,7 +377,10 @@ static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
 
        memset(&hs_ip4addr, 0, sizeof(hs_ip4addr));
        memset(&hs_ip6addr, 0, sizeof(hs_ip6addr));
-       cf_section_parse(cs, home, home_server_config);
+       if (cf_section_parse(cs, home, home_server_config) < 0) {
+               free(home);
+               return 0;
+       }
 
        /*
         *      Figure out which one to use.
@@ -396,8 +409,11 @@ static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
                        goto error;
                }
 
-               free(hs_type);
-               hs_type = NULL;
+               /*
+                *      When CoA is used, the user has to specify the type
+                *      of the home server, even when they point to
+                *      virtual servers.
+                */
                home->secret = strdup("");
                goto skip_port;
 
@@ -432,8 +448,7 @@ static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
                cf_log_err(cf_sectiontoitem(cs),
                           "No shared secret defined for home server %s.",
                           name2);
-               free(home);
-               return 0;
+               goto error;
        }
 
        /*
@@ -444,38 +459,41 @@ static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
 
        if (strcasecmp(hs_type, "auth") == 0) {
                home->type = HOME_TYPE_AUTH;
-               if (type != home->type) {
+               if (pool_type != home->type) {
+               mismatch:
                        cf_log_err(cf_sectiontoitem(cs),
-                                  "Server pool of \"acct\" servers cannot include home server %s of type \"auth\"",
-                                  name2);
-                       free(home);
-                       return 0;
+                                  "Server pool cannot include home server %s of type \"%s\"",
+                                  name2, hs_type);
+                       goto error;
                }
 
        } else if (strcasecmp(hs_type, "acct") == 0) {
                home->type = HOME_TYPE_ACCT;
-               if (type != home->type) {
-                       cf_log_err(cf_sectiontoitem(cs),
-                                  "Server pool of \"auth\" servers cannot include home server %s of type \"acct\"",
-                                  name2);
-                       free(home);
-                       return 0;
-               }
+               if (pool_type != home->type) goto mismatch;
 
        } else if (strcasecmp(hs_type, "auth+acct") == 0) {
                home->type = HOME_TYPE_AUTH;
                dual = TRUE;
 
+#ifdef WITH_COA
+       } else if (strcasecmp(hs_type, "coa") == 0) {
+               home->type = HOME_TYPE_COA;
+               dual = FALSE;
+
+               if (pool_type != home->type) goto mismatch;
+
+               if (home->server != NULL) {
+                       cf_log_err(cf_sectiontoitem(cs),
+                                  "Home servers of type \"coa\" cannot point to a virtual server");
+                       goto error;
+               }
+#endif
+
        } else {
                cf_log_err(cf_sectiontoitem(cs),
                           "Invalid type \"%s\" for home server %s.",
                           hs_type, name2);
-               free(home);
-               free(hs_type);
-               hs_type = NULL;
-               free(hs_check);
-               hs_check = NULL;
-               return 0;
+               goto error;
        }
        free(hs_type);
        hs_type = NULL;
@@ -493,10 +511,7 @@ static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
                cf_log_err(cf_sectiontoitem(cs),
                           "Invalid ping_check \"%s\" for home server %s.",
                           hs_check, name2);
-               free(home);
-               free(hs_check);
-               hs_check = NULL;
-               return 0;
+               goto error;
        }
        free(hs_check);
        hs_check = NULL;
@@ -505,30 +520,27 @@ static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
            (home->ping_check != HOME_PING_CHECK_STATUS_SERVER)) {
                if (!home->ping_user_name) {
                        cf_log_err(cf_sectiontoitem(cs), "You must supply a user name to enable ping checks");
-                       free(home);
-                       return 0;
+                       goto error;
                }
 
                if ((home->type == HOME_TYPE_AUTH) &&
                    !home->ping_user_password) {
                        cf_log_err(cf_sectiontoitem(cs), "You must supply a password to enable ping checks");
-                       free(home);
-                       return 0;
+                       goto error;
                }
        }
 
        if ((home->ipaddr.af != AF_UNSPEC) && /* could be virtual server */
            rbtree_finddata(home_servers_byaddr, home)) {
-               DEBUG2("Ignoring duplicate home server %s.", name2);
-               return 1;
+               cf_log_err(cf_sectiontoitem(cs), "Duplicate home server");
+               goto error;
        }
 
        if (!rbtree_insert(home_servers_byname, home)) {
                cf_log_err(cf_sectiontoitem(cs),
                           "Internal error %d adding home server %s.",
                           __LINE__, name2);
-               free(home);
-               return 0;
+               goto error;
        }
 
        if ((home->ipaddr.af != AF_UNSPEC) && /* could be virtual server */
@@ -537,8 +549,7 @@ static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
                cf_log_err(cf_sectiontoitem(cs),
                           "Internal error %d adding home server %s.",
                           __LINE__, name2);
-               free(home);
-               return 0;
+               goto error;
        }
 
 #ifdef WITH_STATS
@@ -551,8 +562,7 @@ static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
                cf_log_err(cf_sectiontoitem(cs),
                           "Internal error %d adding home server %s.",
                           __LINE__, name2);
-               free(home);
-               return 0;
+               goto error;
        }
 #endif
 
@@ -581,6 +591,20 @@ static int home_server_add(realm_config_t *rc, CONF_SECTION *cs, int type)
        if (home->revive_interval < 60) home->revive_interval = 60;
        if (home->revive_interval > 3600) home->revive_interval = 3600;
 
+#ifdef WITH_COA
+       if (home->coa_irt < 1) home->coa_irt = 1;
+       if (home->coa_irt > 5) home->coa_irt = 5;
+
+       if (home->coa_mrc < 0) home->coa_mrc = 0;
+       if (home->coa_mrc > 20 ) home->coa_mrc = 20;
+
+       if (home->coa_mrt < 0) home->coa_mrt = 0;
+       if (home->coa_mrt > 30 ) home->coa_mrt = 30;
+
+       if (home->coa_mrd < 5) home->coa_mrd = 5;
+       if (home->coa_mrd > 60 ) home->coa_mrd = 60;
+#endif
+
        if (dual) {
                home_server *home2 = rad_malloc(sizeof(*home2));
 
@@ -683,8 +707,6 @@ static int pool_check_home_server(realm_config_t *rc, CONF_PAIR *cp,
        
        home = rbtree_finddata(home_servers_byname, &myhome);
        if (!home) {
-               radlog(L_ERR, "Internal sanity check failed %d",
-                      __LINE__);
                return 0;
        }
 
@@ -752,6 +774,13 @@ static int server_pool_add(realm_config_t *rc,
         */
        cp = cf_pair_find(cs, "fallback");
        if (cp) {
+#ifdef WITH_COA
+               if (server_type == HOME_TYPE_COA) {
+                       cf_log_err(cf_sectiontoitem(cs), "Home server pools of type \"coa\" cannot have a fallback virtual server.");
+                       goto error;
+               }
+#endif
+
                if (!pool_check_home_server(rc, cp, cf_pair_value(cp),
                                            server_type, &pool->fallback)) {
                        
@@ -856,6 +885,8 @@ static int server_pool_add(realm_config_t *rc,
 
        if (do_print) cf_log_info(cs, " }");
 
+       cf_data_add(cs, "home_server_pool", pool, NULL);
+
        rad_assert(pool->server_type != 0);
 
        return 1;
@@ -1578,6 +1609,22 @@ int realms_init(CONF_SECTION *config)
                }
        }
 
+#ifdef WITH_COA
+       /*
+        *      CoA pools aren't tied to realms.
+        */
+       for (cs = cf_subsection_find_next(config, NULL, "home_server_pool");
+            cs != NULL;
+            cs = cf_subsection_find_next(config, cs, "home_server_pool")) {
+               if (cf_data_find(cs, "home_server_pool")) continue;
+
+               if (!server_pool_add(rc, cs, HOME_TYPE_COA, TRUE)) {
+                       return 0;
+               }
+       }
+#endif
+
+
 #ifdef WITH_PROXY
        xlat_register("home_server", xlat_home_server, NULL);
        xlat_register("home_server_pool", xlat_server_pool, NULL);
@@ -1892,6 +1939,18 @@ home_server *home_server_find(fr_ipaddr_t *ipaddr, int port)
        return rbtree_finddata(home_servers_byaddr, &myhome);
 }
 
+#ifdef WITH_COA
+home_server *home_server_byname(const char *name)
+{
+       home_server myhome;
+
+       memset(&myhome, 0, sizeof(myhome));
+       myhome.name = name;
+
+       return rbtree_finddata(home_servers_byname, &myhome);
+}
+#endif
+
 #ifdef WITH_STATS
 home_server *home_server_bynumber(int number)
 {
@@ -1904,4 +1963,15 @@ home_server *home_server_bynumber(int number)
        return rbtree_finddata(home_servers_bynumber, &myhome);
 }
 #endif
+
+home_pool_t *home_pool_byname(const char *name, int type)
+{
+       home_pool_t mypool;
+       
+       memset(&mypool, 0, sizeof(mypool));
+       mypool.name = name;
+       mypool.server_type = type;
+       return rbtree_finddata(home_pools_byname, &mypool);
+}
+
 #endif
index 9041c51..29e18bd 100644 (file)
@@ -197,6 +197,10 @@ void request_free(REQUEST **request_ptr)
 
        request = *request_ptr;
 
+       rad_assert(!request->in_request_hash);
+       rad_assert(!request->in_proxy_hash);
+       rad_assert(!request->ev);
+
        if (request->packet)
                rad_free(&request->packet);
 
@@ -238,6 +242,17 @@ void request_free(REQUEST **request_ptr)
                request->root = NULL;
        }
 
+#ifdef WITH_COA
+       if (request->coa) {
+               request->coa->parent = NULL;
+               request_free(&request->coa);
+       }
+
+       if (request->parent && (request->parent->coa == request)) {
+               request->parent->coa = NULL;
+       }
+#endif
+
 #ifndef NDEBUG
        request->magic = 0x01020304;    /* set the request to be nonsense */
 #endif
@@ -468,6 +483,19 @@ REQUEST *request_alloc_fake(REQUEST *request)
   return fake;
 }
 
+#ifdef WITH_COA
+REQUEST *request_alloc_coa(REQUEST *request)
+{
+       if (!request || request->coa) return NULL;
+
+       request->coa = request_alloc_fake(request);
+       request->coa->packet->code = 0; /* unknown, as of yet */
+       request->coa->child_state = REQUEST_RUNNING;
+       request->coa->proxy = rad_alloc(0);
+
+       return request->coa;
+}
+#endif
 
 /*
  *     Copy a quoted string.