diff --git a/Makefile b/Makefile index 8ef638e1117cc..25e08e079f896 100644 --- a/Makefile +++ b/Makefile @@ -92,7 +92,8 @@ $(BUILD_DIR)/tests/radiusd-c: raddb/test.conf ${BUILD_DIR}/bin/radiusd | build.r @echo "ok" @touch $@ -test: ${BUILD_DIR}/bin/radiusd ${BUILD_DIR}/bin/radclient tests.unit tests.xlat tests.keywords tests.auth $(BUILD_DIR)/tests/radiusd-c | build.raddb +test: ${BUILD_DIR}/bin/radiusd ${BUILD_DIR}/bin/radclient tests.unit tests.xlat \ + tests.keywords tests.auth tests.radsec $(BUILD_DIR)/tests/radiusd-c | build.raddb @$(MAKE) -C src/tests tests #  Tests specifically for CI.  We do a LOT more than just diff --git a/doc/source/Doxyfile b/doc/source/Doxyfile index 010a21ffec14b..8e5d8d63fef99 100644 --- a/doc/source/Doxyfile +++ b/doc/source/Doxyfile @@ -1939,6 +1939,7 @@ PREDEFINED = WITH_PROXY \ WITH_STATS \ WITH_COMMAND_SOCKET \ WITH_COA \ + WITH_COA_SINGLE_TUNNEL \ WITH_TCP \ WITH_DHCP \ WITH_VMPS \ diff --git a/raddb/proxy.conf b/raddb/proxy.conf index 7f5ff2e1526e7..48e40e5ff88ac 100644 --- a/raddb/proxy.conf +++ b/raddb/proxy.conf @@ -146,12 +146,16 @@ home_server localhost { # or Accounting-Request packets. # # Allowed values are: - # auth - Handles Access-Request packets - # 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 + # auth - Handles Access-Request packets + # acct - Handles Accounting-Request packets + # auth+acct - Handles Access-Request packets at "port", + # and Accounting-Request packets at "port + 1" + # auth+coa - Handles packets as specified for auth type + # and handles incoming CoA-Request and Disconnect-Request packets + # auth+acct+coa - Handles packets as specified for auth+acct type + # and handles incoming CoA-Request and Disconnect-Request packets + # coa - Handles outgoing CoA-Request and Disconnect-Request packets. + # See also raddb/sites-available/originate-coa type = auth # @@ -441,9 +445,10 @@ home_server localhost { max_outstanding = 65536 # - # The configuration items in the next sub-section are used ONLY - # when "type = coa". It is ignored for all other type of home - # servers. + # The configuration items in the next sub-section are used ONLY when + # "type = coa/auth+coa/auth+acct+coa". It is ignored for all other type of + # home servers. For auth+coa/auth+acct+coa types the virtual server + # configuration is relevant only. # # See RFC 5080 for the definitions of the following terms. # RAND is a function (internal to FreeRADIUS) returning @@ -477,6 +482,13 @@ home_server localhost { # Maximum Retransmit Duration: 5..60 mrd = 30 + + # A virtual_server may be specified here for types auth+coa and + # auth+acct+coa. If the virtual server is specified, a "recv-coa" + # section is called when CoA-Request or Disconnect-Request are received + # and a "send-coa" section is called when the CoA-ACK/NAK or + # Disconnect-ACK/NAK are sent. + #virtual_server = pre_post_proxy } # diff --git a/raddb/sites-available/coa-relay b/raddb/sites-available/coa-relay index 05fd6bc44650f..91e5e81e1580a 100644 --- a/raddb/sites-available/coa-relay +++ b/raddb/sites-available/coa-relay @@ -202,6 +202,9 @@ server coa-buffered-reader { &COA-Packet-DST-IP-Address disconnect:Packet-DST-Port := \ &COA-Packet-DST-Port + # Alternatively TCP-Session-Key may be specified + #disconnect:TCP-Session-Key := \ + # &Operator-NAS-Identifier disconnect:Acct-Session-Id := \ &COA-Acct-Session-Id @@ -224,6 +227,9 @@ server coa-buffered-reader { &COA-Packet-DST-IP-Address coa:Packet-DST-Port := \ &COA-Packet-DST-Port + # Alternatively TCP-Session-Key may be specified + #coa:TCP-Session-Key := \ + # &Operator-NAS-Identifier coa:Acct-Session-Id := \ &COA-Acct-Session-Id diff --git a/raddb/sites-available/default b/raddb/sites-available/default index 6d0318a9b9f47..42cb2e7a0b833 100644 --- a/raddb/sites-available/default +++ b/raddb/sites-available/default @@ -61,7 +61,7 @@ listen { # Allowed values are: # auth listen for authentication packets # acct listen for accounting packets - # auth+acct listen for both authentication and accounting packets + # auth+acct listen for both authentication and accounting packets # proxy IP to use for sending proxied packets # detail Read from the detail file. For examples, see # raddb/sites-available/copy-acct-to-home-server @@ -463,9 +463,12 @@ authorize { # through the following section, and ONLY the following section. # This permits you to do DB queries, for example. If the modules # listed here return "fail", then NO response is sent. + # For auth+coa and auth+acct+coa listeners we may set up TCP-Session-Key. # # Autz-Type Status-Server { -# +# update control { +# TCP-Session-Key := &Operator-NAS-Identifier +# } # } } diff --git a/raddb/sites-available/tls b/raddb/sites-available/tls index f0d0149f06e21..ba8635f324c6d 100644 --- a/raddb/sites-available/tls +++ b/raddb/sites-available/tls @@ -46,13 +46,20 @@ listen { port = 2083 # - # TCP and TLS sockets can accept Access-Request and - # Accounting-Request on the same socket. + # TCP and TLS sockets can accept Access-Request, Accounting-Request. + # TLS socket may also send CoA-Request, Disconnect-Request and accept + # CoA-ACK/NAK, Disconnect-ACK/NAK on the same socket. # - # auth = only Access-Request - # acct = only Accounting-Request - # auth+acct = both - * coa = only CoA / Disconnect requests + # auth = only Access-Request + # auth+coa = Access-Request and + # send CoA-Requet and Disconnect-Request, and + # accept CoA-ACK/NAK, Disconnect-ACK/NAK + # acct = only Accounting-Request + # auth+acct = both Access-Request and Accounting-Request + # auth+acct+coa = all: Access-Request, Accounting-Request, and + # send CoA-Requet and Disconnect-Request, and + # accept CoA-ACK/NAK, Disconnect-ACK/NAK + # coa = only CoA / Disconnect requests # type = auth+acct @@ -391,6 +398,15 @@ listen { # client = "/path/to/openssl verify -CApath ${..ca_path} %{TLS-Client-Cert-Filename}" } } + + # CoA specific parameters for auth+coa and auth+acct+coa types. See + # raddb/proxy.conf for details. + coa { + irt = 2 + mrt = 16 + mrc = 5 + mrd = 30 + } } clients radsec { @@ -443,7 +459,7 @@ home_server tls { port = 2083 # type can be the same types as for the "listen" section/ - # e.g. auth, acct, auth+acct, coa + # e.g. auth, acct, auth+acct, auth+coa, auth+acct+coa, coa type = auth secret = radsec proto = tcp diff --git a/share/dictionary.freeradius.internal b/share/dictionary.freeradius.internal index 64a9bf44220df..dbdd41f3508b6 100644 --- a/share/dictionary.freeradius.internal +++ b/share/dictionary.freeradius.internal @@ -287,6 +287,8 @@ ATTRIBUTE SSHA3-512-Password 1185 octets ATTRIBUTE MS-CHAP-Peer-Challenge 1192 octets ATTRIBUTE Home-Server-Name 1193 string +ATTRIBUTE TCP-Session-Key 1194 string + # # Range: 1200-1279 # EAP-SIM (and other EAP type) weirdness. diff --git a/src/include/features-h b/src/include/features-h index f24a52d20edb8..c6a925cc441c8 100644 --- a/src/include/features-h +++ b/src/include/features-h @@ -65,3 +65,17 @@ # endif # endif #endif + +#ifndef WITHOUT_COA_SINGLE_TUNNEL +# define WITH_COA_SINGLE_TUNNEL (1) +# ifndef WITH_COA +# error WITH_COA_SINGLE_TUNNEL requires WITH_COA +# endif +# ifndef WITH_TCP +# error WITH_COA_SINGLE_TUNNEL requires WITH_TCP +# endif +# ifndef WITH_TLS +# error WITH_COA_SINGLE_TUNNEL requires WITH_TLS +# endif +#endif + diff --git a/src/include/listen.h b/src/include/listen.h index 4f50bbf8088b0..a06b9c61c4b26 100644 --- a/src/include/listen.h +++ b/src/include/listen.h @@ -73,6 +73,16 @@ struct rad_listen { bool dual; rbtree_t *children; rad_listen_t *parent; +# ifdef WITH_COA_SINGLE_TUNNEL + bool with_coa; + char const *key; /* TCP-Session-Key */ + + uint32_t coa_irt; + uint32_t coa_mrc; + uint32_t coa_mrt; + uint32_t coa_mrd; +# endif /* WITH_COA_SINGLE_TUNNEL */ + #endif bool nodup; bool synchronous; @@ -83,9 +93,16 @@ struct rad_listen { #endif rad_listen_recv_t recv; + rad_listen_send_t send; + rad_listen_send_t send_proxy; + rad_listen_encode_t encode; + rad_listen_encode_t encode_proxy; + rad_listen_decode_t decode; + rad_listen_decode_t decode_proxy; + rad_listen_print_t print; CONF_SECTION const *cs; @@ -118,7 +135,8 @@ typedef struct listen_socket_t { uint32_t rate_pps_now; uint32_t max_rate; - /* for outgoing sockets */ + /* for outgoing sockets, + * home is used for incoming socket as well if type is auth+coa/auth+acct+coa */ home_server_t *home; fr_ipaddr_t other_ipaddr; uint16_t other_port; @@ -134,6 +152,7 @@ typedef struct listen_socket_t { fr_socket_limit_t limit; struct listen_socket_t *parent; + /* for outgoing sockets as well if type is auth+coa/auth+acct+coa */ RADCLIENT *client; RADIUS_PACKET *packet; /* for reading partial packets */ diff --git a/src/include/radiusd.h b/src/include/radiusd.h index 028202fe2cc8e..ed9dcee6089bb 100644 --- a/src/include/radiusd.h +++ b/src/include/radiusd.h @@ -562,6 +562,7 @@ void hup_logfile(void); /* listen.c */ void listen_free(rad_listen_t **head); +void listen_destroy(void); int listen_init(CONF_SECTION *cs, rad_listen_t **head, bool spawn_flag); rad_listen_t *proxy_new_listener(TALLOC_CTX *ctx, home_server_t *home, uint16_t src_port); RADCLIENT *client_listener_find(rad_listen_t *listener, fr_ipaddr_t const *ipaddr, uint16_t src_port); @@ -570,6 +571,11 @@ RADCLIENT *client_listener_find(rad_listen_t *listener, fr_ipaddr_t const *ipadd RADCLIENT_LIST *listener_find_client_list(fr_ipaddr_t const *ipaddr, uint16_t port, int proto); #endif rad_listen_t *listener_find_byipaddr(fr_ipaddr_t const *ipaddr, uint16_t port, int proto); +#ifdef WITH_COA_SINGLE_TUNNEL +void listener_store_bykey(rad_listen_t *listener, const char *newkey); +rad_listen_t *listener_find_bykey(char const *key, size_t *listcount); +rad_listen_t *listener_find_byaddr(fr_ipaddr_t const *ipaddr, uint16_t port, size_t *listcount); +#endif int rad_status_server(REQUEST *request); /* event.c */ @@ -605,6 +611,10 @@ int dual_tls_recv(rad_listen_t *listener); int dual_tls_send(rad_listen_t *listener, REQUEST *request); int proxy_tls_recv(rad_listen_t *listener); int proxy_tls_send(rad_listen_t *listener, REQUEST *request); +#ifdef WITH_COA_SINGLE_TUNNEL +int dual_tls_send_req(rad_listen_t *listener, REQUEST *request); +int proxy_tls_send_reply(rad_listen_t *listener, REQUEST *request); +#endif #endif /* diff --git a/src/include/realms.h b/src/include/realms.h index dcb420371e5f1..33a0cd1c75f70 100644 --- a/src/include/realms.h +++ b/src/include/realms.h @@ -19,11 +19,15 @@ extern bool home_servers_udp; //!< Whether there are any UDP home servers typedef enum { HOME_TYPE_INVALID = 0, - HOME_TYPE_AUTH, //!< Authentication server - HOME_TYPE_ACCT, //!< Accounting server - HOME_TYPE_AUTH_ACCT //!< Authentication and accounting server + HOME_TYPE_AUTH, //!< Authentication server + HOME_TYPE_ACCT, //!< Accounting server + HOME_TYPE_AUTH_ACCT //!< Authentication and accounting server #ifdef WITH_COA +#ifdef WITH_COA_SINGLE_TUNNEL + ,HOME_TYPE_AUTH_COA //!< Authentication server and CoA client + ,HOME_TYPE_AUTH_ACCT_COA //!< Authentication, accounting server and CoA client +#endif ,HOME_TYPE_COA //!< CoA destination (NAS or Proxy) #endif } home_type_t; @@ -59,6 +63,9 @@ typedef struct home_server { //!< stats or when specifying home servers for a pool. bool dual; //!< One of a pair of homeservers on consecutive ports. +#ifdef WITH_COA_SINGLE_TUNNEL + bool with_coa; //!< Accept CoA requests from the home server +#endif bool dynamic; //!< is this a dynamically added home server? char const *server; //!< For internal proxying char const *parent_server; @@ -120,6 +127,9 @@ typedef struct home_server { uint32_t coa_mrc; uint32_t coa_mrt; uint32_t coa_mrd; +# ifdef WITH_COA_SINGLE_TUNNEL + char const *coa_server; //!< For accepting incoming coa requests +# endif #endif #ifdef WITH_TLS fr_tls_server_conf_t *tls; diff --git a/src/lib/packet.c b/src/lib/packet.c index d19c4c0522e0e..c2c089ab04ac7 100644 --- a/src/lib/packet.c +++ b/src/lib/packet.c @@ -697,6 +697,14 @@ bool fr_packet_list_id_alloc(fr_packet_list_t *pl, int proto, */ if (ps->num_outgoing == 256) continue; +#ifdef WITH_COA_SINGLE_TUNNEL + if(pctx && *pctx) { + if(ps->ctx == *pctx) goto skip_search; + + continue; + } +#endif + #ifdef WITH_TCP if (ps->proto != proto) continue; #endif @@ -760,6 +768,9 @@ bool fr_packet_list_id_alloc(fr_packet_list_t *pl, int proto, * Otherwise, this socket is OK to use. */ +#ifdef WITH_COA_SINGLE_TUNNEL +skip_search: +#endif /* * Look for a free Id, starting from a random number. */ diff --git a/src/main/auth.c b/src/main/auth.c index d43d65e46e83c..881e076a6388f 100644 --- a/src/main/auth.c +++ b/src/main/auth.c @@ -596,6 +596,18 @@ int rad_authenticate(REQUEST *request) authenticate: #endif +#ifdef WITH_COA_SINGLE_TUNNEL + if(request->listener->with_coa && + (tmp = fr_pair_find_by_num(request->config, PW_TCP_SESSION_KEY, 0, TAG_ANY))) { + char const *key = tmp->vp_strvalue; + /* do not overwrite */ + if(!request->listener->key) { + RDEBUG2("Set current tunnel with key %s", key); + listener_store_bykey(request->listener, key); + } + } +#endif + /* * Validate the user */ diff --git a/src/main/command.c b/src/main/command.c index 9c24a572a3b94..7ed57bd381ddc 100644 --- a/src/main/command.c +++ b/src/main/command.c @@ -1989,8 +1989,11 @@ static int command_inject_file(rad_listen_t *listener, int argc, char *argv[]) * Re-write the IO for the listener. */ fake->encode = null_socket_dencode; + fake->encode_proxy = null_socket_dencode; fake->decode = null_socket_dencode; + fake->decode_proxy = null_socket_dencode; fake->send = null_socket_send; + fake->send_proxy = null_socket_send; packet = rad_alloc(NULL, false); packet->src_ipaddr = sock->src_ipaddr; diff --git a/src/main/listen.c b/src/main/listen.c index e09518e3fab9c..329548ec70e67 100644 --- a/src/main/listen.c +++ b/src/main/listen.c @@ -52,6 +52,33 @@ RCSID("$Id$") #include #endif + +#ifdef WITH_COA_SINGLE_TUNNEL + +#include +pthread_mutex_t tree_keymutex; +pthread_mutex_t tree_addrmutex; +pthread_mutexattr_t tree_mutexattr; + +static rbtree_t *tree_key = NULL; /* listener instance by its key (TCP-Session-Key) */ +static rbtree_t *tree_addr = NULL; /* listener instance by its ip address */ + +typedef struct radlisten_node_t radlisten_node_t; +typedef struct radlisten_list_t radlisten_list_t; + +struct radlisten_node_t { + radlisten_node_t *next; + rad_listen_t *listener; +}; + +struct radlisten_list_t { + radlisten_node_t *head; + radlisten_node_t *tail; + radlisten_node_t *current; /* to round-robin */ + size_t num; +}; +#endif /* WITH_COA_SINGLE_TUNNEL */ + #ifdef DEBUG_PRINT_PACKET static void print_packet(RADIUS_PACKET *packet) { @@ -67,6 +94,11 @@ static void print_packet(RADIUS_PACKET *packet) #endif +#ifdef WITH_COA_SINGLE_TUNNEL +static void radlisten_list_free(void *list); +static void listener_forget(rad_listen_t *listener); +static void listener_store_byaddr(rad_listen_t *listener, fr_ipaddr_t const *ipaddr); +#endif static rad_listen_t *listen_alloc(TALLOC_CTX *ctx, RAD_LISTEN_TYPE type); #ifdef WITH_COMMAND_SOCKET @@ -359,6 +391,9 @@ int rad_status_server(REQUEST *request) { int rcode = RLM_MODULE_OK; DICT_VALUE *dval; +#ifdef WITH_COA_SINGLE_TUNNEL + VALUE_PAIR *vp = NULL; +#endif switch (request->listener->type) { #ifdef WITH_STATS @@ -368,6 +403,17 @@ int rad_status_server(REQUEST *request) dval = dict_valbyname(PW_AUTZ_TYPE, 0, "Status-Server"); if (dval) { rcode = process_authorize(dval->value, request); +#ifdef WITH_COA_SINGLE_TUNNEL + if(request->listener->with_coa && + (vp = fr_pair_find_by_num(request->config, PW_TCP_SESSION_KEY, 0, TAG_ANY))) { + char const *key = vp->vp_strvalue; + /* do not overwrite */ + if(!request->listener->key) { + RDEBUG2("Set current tunnel by key %s", key); + listener_store_bykey(request->listener, key); + } + } +#endif } else { rcode = RLM_MODULE_OK; } @@ -735,6 +781,33 @@ static int dual_tcp_accept(rad_listen_t *listener) #endif } +#ifdef WITH_COA_SINGLE_TUNNEL + if(this->with_coa) { + + this->send_proxy = dual_tls_send_req; + this->encode_proxy = master_listen[RAD_LISTEN_PROXY].encode; + this->decode_proxy = master_listen[RAD_LISTEN_PROXY].decode; + + listener_store_byaddr(this, &sock->other_ipaddr); + + home_server_t *home = talloc_zero(this, home_server_t); + { + home->ipaddr = sock->other_ipaddr; + home->port = sock->other_port; + home->proto = sock->proto; + home->secret = sock->client->secret; + + home->coa_irt = this->coa_irt; + home->coa_mrt = this->coa_mrt; + home->coa_mrc = this->coa_mrc; + home->coa_mrd = this->coa_mrd; + home->coa_server = this->server; + } + + sock->home = home; + } +#endif + /* * FIXME: set O_NONBLOCK on the accept'd fd. * See djb's portability rants for details. @@ -797,6 +870,12 @@ int common_socket_print(rad_listen_t const *this, char *buffer, size_t bufsize) } #endif +#ifdef WITH_COA_SINGLE_TUNNEL + if(this->with_coa) { + ADDSTRING("+coa"); + } +#endif + if (sock->interface) { ADDSTRING(" interface "); ADDSTRING(sock->interface); @@ -932,6 +1011,16 @@ static CONF_PARSER limit_config[] = { CONF_PARSER_TERMINATOR }; +#ifdef WITH_COA_SINGLE_TUNNEL +static CONF_PARSER coa_config[] = { + { "irt", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_irt), STRINGIFY(2) }, + { "mrt", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_mrt), STRINGIFY(16) }, + { "mrc", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_mrc), STRINGIFY(5) }, + { "mrd", FR_CONF_OFFSET(PW_TYPE_INTEGER, rad_listen_t, coa_mrd), STRINGIFY(30) }, + CONF_PARSER_TERMINATOR +}; +#endif + #ifdef WITH_TCP /* @@ -951,15 +1040,61 @@ static int listener_cmp(void const *one, void const *two) return 0; } +# ifdef WITH_COA_SINGLE_TUNNEL +static int listenernode_key_cmp(void const *one, void const *two) +{ + rad_listen_t const *a = ((radlisten_node_t const*)one)->listener; + rad_listen_t const *b = ((radlisten_node_t const*)two)->listener; + + rad_assert(a->key && b->key); + + return strcmp(a->key, b->key); + +} + +static int listenerlist_key_cmp(void const *one, void const *two) +{ + radlisten_list_t const *a = one; + radlisten_list_t const *b = two; + + rad_assert(a->head && b->head); + + return listenernode_key_cmp(a->head, b->head); +} + +static int listenernode_addr_cmp(void const *one, void const *two) +{ + rad_listen_t const *a = ((radlisten_node_t const*)one)->listener; + rad_listen_t const *b = ((radlisten_node_t const*)two)->listener; + + rad_assert(a->data && b->data); + + fr_ipaddr_t aip = ((listen_socket_t*)a->data)->other_ipaddr; + fr_ipaddr_t bip = ((listen_socket_t*)b->data)->other_ipaddr; + + return fr_ipaddr_cmp(&aip, &bip); +} + +static int listenerlist_addr_cmp(void const *one, void const *two) +{ + radlisten_list_t const *a = one; + radlisten_list_t const *b = two; + + rad_assert(a->head && b->head); + + return listenernode_addr_cmp(a->head, b->head); +} +# endif /* WITH_COA_SINGLE_TUNNEL */ + static int listener_unlink(UNUSED void *ctx, UNUSED void *data) { return 2; /* unlink this node from the tree */ } -#endif +#endif /* WITH_TCP */ /* - * Parse an authentication or accounting socket. + * Parse an status/proxy/auth/acct/coa sockets. */ int common_socket_parse(CONF_SECTION *cs, rad_listen_t *this) { @@ -1404,7 +1539,7 @@ static int acct_socket_send(rad_listen_t *listener, REQUEST *request) static int proxy_socket_send(rad_listen_t *listener, REQUEST *request) { rad_assert(request->proxy_listener == listener); - rad_assert(listener->send == proxy_socket_send); + rad_assert(listener->send_proxy == proxy_socket_send); if (rad_send(request->proxy, NULL, request->home_server->secret) < 0) { @@ -2739,6 +2874,10 @@ static int _listener_free(rad_listen_t *this) rbtree_walk(this->children, RBTREE_DELETE_ORDER, listener_unlink, this); } +#ifdef WITH_COA_SINGLE_TUNNEL + if(this->with_coa) listener_forget(this); +#endif + #ifdef WITH_TLS /* * Note that we do NOT free this->tls, as the @@ -2764,7 +2903,6 @@ static int _listener_free(rad_listen_t *this) return 0; } - /* * Allocate & initialize a new listener. */ @@ -2776,10 +2914,17 @@ static rad_listen_t *listen_alloc(TALLOC_CTX *ctx, RAD_LISTEN_TYPE type) this->type = type; this->recv = master_listen[this->type].recv; - this->send = master_listen[this->type].send; this->print = master_listen[this->type].print; - this->encode = master_listen[this->type].encode; - this->decode = master_listen[this->type].decode; + + if(type == RAD_LISTEN_PROXY) { + this->send_proxy = master_listen[this->type].send; + this->encode_proxy = master_listen[this->type].encode; + this->decode_proxy = master_listen[this->type].decode; + } else { + this->send = master_listen[this->type].send; + this->encode = master_listen[this->type].encode; + this->decode = master_listen[this->type].decode; + } talloc_set_destructor(this, _listener_free); @@ -2877,10 +3022,33 @@ rad_listen_t *proxy_new_listener(TALLOC_CTX *ctx, home_server_t *home, uint16_t } this->recv = proxy_tls_recv; - this->send = proxy_tls_send; + this->send_proxy = proxy_tls_send; + +# ifdef WITH_COA_SINGLE_TUNNEL + if(home->with_coa) { + + this->with_coa = home->with_coa; + + this->send = proxy_tls_send_reply; + this->encode = master_listen[RAD_LISTEN_AUTH].encode; + this->decode = master_listen[RAD_LISTEN_AUTH].decode; + + RADCLIENT *client = talloc_zero(sock, RADCLIENT); + { + client->ipaddr = sock->other_ipaddr; + client->src_ipaddr = sock->my_ipaddr; + client->longname = client->shortname = talloc_typed_strdup(client, home->name); + client->secret = talloc_typed_strdup(client, home->secret); + client->nas_type = "none"; + client->server = talloc_typed_strdup(client, home->coa_server); + } + + sock->client = client; + } +# endif } -#endif -#endif +#endif /* WITH_TLS */ +#endif /* WITH_TCP */ /* * Figure out which port we were bound to. */ @@ -2924,9 +3092,15 @@ static const FR_NAME_NUMBER listen_compare[] = { { "status", RAD_LISTEN_NONE }, #endif { "auth", RAD_LISTEN_AUTH }, +#ifdef WITH_COA_SINGLE_TUNNEL + { "auth+coa", RAD_LISTEN_AUTH }, +#endif #ifdef WITH_ACCOUNTING - { "acct", RAD_LISTEN_ACCT }, - { "auth+acct", RAD_LISTEN_AUTH }, + { "acct", RAD_LISTEN_ACCT }, + { "auth+acct", RAD_LISTEN_AUTH }, +# ifdef WITH_COA_SINGLE_TUNNEL + { "auth+acct+coa", RAD_LISTEN_AUTH }, +# endif #endif #ifdef WITH_DETAIL { "detail", RAD_LISTEN_DETAIL }, @@ -2964,6 +3138,9 @@ static rad_listen_t *listen_parse(CONF_SECTION *cs, char const *server) char const *value; fr_dlhandle handle; CONF_SECTION *server_cs; +#ifdef WITH_COA_SINGLE_TUNNEL + CONF_SECTION *coa; +#endif char buffer[32]; cp = cf_pair_find(cs, "type"); @@ -3074,10 +3251,19 @@ static rad_listen_t *listen_parse(CONF_SECTION *cs, char const *server) #ifdef WITH_TCP /* - * Special-case '+' for "auth+acct". + * Special-case '+' for "auth+acct" and "auth+coa"/"auth+acct+coa" */ - if (strchr(listen_type, '+') != NULL) { + char *plus_loc = NULL; + if ((plus_loc = strchr(listen_type, '+')) != NULL) { this->dual = true; +# ifdef WITH_COA_SINGLE_TUNNEL + if (strchr(plus_loc + 1, '+') != NULL) { + this->with_coa = true; + } else if (strcmp(plus_loc + 1, "coa") == 0) { + this->dual = false; + this->with_coa = true; + } +# endif } #endif @@ -3097,6 +3283,42 @@ static rad_listen_t *listen_parse(CONF_SECTION *cs, char const *server) return NULL; } +#ifdef WITH_COA_SINGLE_TUNNEL + if(this->with_coa && !this->tls) { + cf_log_err_cs(cs, "Type \"+coa\" is available for TLS transport only"); + listen_free(&this); + return NULL; + } + + coa = cf_section_sub_find(cs, "coa"); + if (coa) { + if(!this->with_coa) { + cf_log_err_cs(cs, "Invalid section \"coa\" for this listener type"); + listen_free(&this); + return NULL; + } + + rcode = cf_section_parse(cs, this, coa_config); + if (rcode < 0) { + listen_free(&this); + return NULL; + } + + /* + * Same boundary checks as for home server + */ + FR_INTEGER_BOUND_CHECK("coa_irt", this->coa_irt, >=, 1); + FR_INTEGER_BOUND_CHECK("coa_irt", this->coa_irt, <=, 5); + + FR_INTEGER_BOUND_CHECK("coa_mrc", this->coa_mrc, <=, 20); + + FR_INTEGER_BOUND_CHECK("coa_mrt", this->coa_mrt, <=, 30); + + FR_INTEGER_BOUND_CHECK("coa_mrd", this->coa_mrd, >=, 5); + FR_INTEGER_BOUND_CHECK("coa_mrd", this->coa_mrd, <=, 60); + } +#endif /* WITH_COA_SINGLE_TUNNEL */ + cf_log_info(cs, "}"); return this; @@ -3398,6 +3620,40 @@ int listen_init(CONF_SECTION *config, rad_listen_t **head, bool spawn_flag) } } +#ifdef WITH_COA_SINGLE_TUNNEL + if(pthread_mutexattr_init(&tree_mutexattr) != 0 + || pthread_mutexattr_settype(&tree_mutexattr, PTHREAD_MUTEX_RECURSIVE) != 0) + { + ERROR("FATAL: Failed to initialize tree mutex attributes: %s", + fr_syserror(errno)); + fr_exit(1); + } + + if (pthread_mutex_init(&tree_keymutex, &tree_mutexattr) != 0) + { + ERROR("FATAL: Failed to initialize tree key mutex: %s", + fr_syserror(errno)); + fr_exit(1); + } + + if (pthread_mutex_init(&tree_addrmutex, &tree_mutexattr) != 0) + { + ERROR("FATAL: Failed to initialize tree key mutex: %s", + fr_syserror(errno)); + fr_exit(1); + } + + tree_key = + rbtree_create(NULL, listenerlist_key_cmp, radlisten_list_free, RBTREE_FLAG_REPLACE); + tree_addr = + rbtree_create(NULL, listenerlist_addr_cmp, radlisten_list_free, RBTREE_FLAG_REPLACE); + + if (!tree_addr || !tree_addr) { + ERROR("Out of memory"); + fr_exit(1); + } +#endif /* WITH_COA_SINGLE_TUNNEL */ + /* * Haven't defined any sockets. Die. */ @@ -3425,6 +3681,17 @@ void listen_free(rad_listen_t **head) *head = NULL; } +void listen_destroy(void) +{ +#ifdef WITH_COA_SINGLE_TUNNEL + rbtree_free(tree_key), rbtree_free(tree_addr); + tree_key = NULL, tree_addr = NULL; + pthread_mutex_destroy(&tree_keymutex); + pthread_mutex_destroy(&tree_addrmutex); + pthread_mutexattr_destroy(&tree_mutexattr); +#endif +} + #ifdef WITH_STATS RADCLIENT_LIST *listener_find_client_list(fr_ipaddr_t const *ipaddr, uint16_t port, int proto) { @@ -3488,3 +3755,235 @@ rad_listen_t *listener_find_byipaddr(fr_ipaddr_t const *ipaddr, uint16_t port, i return NULL; } + +#ifdef WITH_COA_SINGLE_TUNNEL +static void radlisten_list_free(void *list) +{ + radlisten_list_t *l = (radlisten_list_t*)list; + { + l->head = NULL; + l->tail = NULL; + l->current = NULL; + l->num = 0; + } + + talloc_free(l); +} + +static radlisten_list_t *listener_findlist_bykey(char const *key) +{ + rad_listen_t mylistener; + radlisten_node_t mynode = { NULL /* next */, &mylistener}; + radlisten_list_t mylisteners = { &mynode, NULL, NULL, 1 /* num */ }; + radlisten_list_t *ret = NULL; + + mylistener.key = key; + + ret = rbtree_finddata(tree_key, &mylisteners); + + return ret; +} + +rad_listen_t *listener_find_bykey(char const *key, size_t *listcount) +{ + rad_listen_t *ret = NULL; + radlisten_list_t *list; + + pthread_mutex_lock(&tree_keymutex); + + list = listener_findlist_bykey(key); + if(list) { /* select round-robin */ + /* + * Fresh list current points to head, so iteration + * starts from the second node, but that's just fine + */ + rad_assert(list->current); + radlisten_node_t *next = list->current->next; + if(!next) next = list->head; /* end of the list */ + + list->current = next; + ret = next->listener; + + if(listcount) *listcount = list->num; + } + + pthread_mutex_unlock(&tree_keymutex); + + return ret; +} + +static radlisten_list_t *listener_findlist_byaddr(fr_ipaddr_t const *ipaddr) +{ + listen_socket_t mysocket; + rad_listen_t mylistener; + radlisten_node_t mynode = { NULL /* next */, &mylistener}; + radlisten_list_t mylisteners = { &mynode, NULL, NULL, 1 /* num */}; + + mysocket.other_ipaddr = *ipaddr; + mylistener.data = &mysocket; + + return rbtree_finddata(tree_addr, &mylisteners); +} + +rad_listen_t *listener_find_byaddr(fr_ipaddr_t const *ipaddr, uint16_t port, size_t *listcount) +{ + rad_listen_t *ret = NULL; + radlisten_list_t *list; + + pthread_mutex_lock(&tree_addrmutex); + + list = listener_findlist_byaddr(ipaddr); + if(list) { + if(port) { /* find exact listener */ + radlisten_node_t *it = list->head; + while(it) { + uint16_t lport = ((listen_socket_t*)it->listener->data)->other_port; + if(port == lport) { + ret = it->listener; + break; + } + it = it->next; + } + } else { /* select round-robin */ + rad_assert(list->current); + radlisten_node_t *next = list->current->next; + if(!next) next = list->head; /* end of the list */ + + list->current = next; + ret = next->listener; + } + + if(listcount) *listcount = list->num; + } + + pthread_mutex_unlock(&tree_addrmutex); + return ret; +} + +static void listener_remove_fromtree( + rbtree_t *tree, pthread_mutex_t *tree_mutex, rad_listen_t *listener) +{ + void* rbnode; + radlisten_list_t *list; + + radlisten_node_t *prev = NULL, *it; + + radlisten_node_t mynode = { NULL, listener }; + radlisten_list_t mylisteners = { &mynode, NULL, NULL, 1 /* num */ }; + + pthread_mutex_lock(tree_mutex); + + rbnode = rbtree_find(tree, &mylisteners); + list = rbtree_node2data(tree, rbnode); + + if(!list) goto out; /* key may not exist yet */ + + it = list->head; + + while(it && it->listener != listener) { + prev = it, it = it->next; + } + + if(it) { + radlisten_node_t* next = it->next; + + talloc_free(it); + + list->num--; + + /* list is [0, n] */ + if(prev) { /* 0 < it <= n */ + prev->next = next; + if(list->tail == it) list->tail = prev; + if(list->current == it) list->current = prev; + } else if(next) { /* it == 0, n > 0 */ + list->head = next; + if(list->current == it) list->current = next; + } else { /* single element list */ + rbtree_delete(tree, rbnode); /* lists and nodes are removed as well */ + } + + } else { + DEBUG("Cannot find listener in list"); + } + +out: + pthread_mutex_unlock(tree_mutex); +} + +static void listener_forget(rad_listen_t *listener) +{ + if(!listener->with_coa) return; + + listener_remove_fromtree(tree_key, &tree_keymutex, listener); + listener_remove_fromtree(tree_addr, &tree_addrmutex, listener); +} + +static radlisten_node_t* radlisten_node_init(radlisten_list_t *list, rad_listen_t *listener) +{ + radlisten_node_t *node = talloc_zero(list, radlisten_node_t); + node->next = NULL; + node->listener = listener; + + return node; +} + +static void listener_store_byaddr(rad_listen_t *listener, fr_ipaddr_t const *ipaddr) +{ + radlisten_list_t *list; + + if(!listener->with_coa) return; + + pthread_mutex_lock(&tree_addrmutex); + + list = listener_findlist_byaddr(ipaddr); + + if(list) { + radlisten_node_t *node = radlisten_node_init(list, listener); + list->tail->next = node; + list->tail = node; + } else { + list = talloc_zero(tree_addr, radlisten_list_t); + list->head = radlisten_node_init(list, listener); + list->tail = list->head; + list->current = list->head; + rbtree_insert(tree_addr, list); + } + + list->num++; + + pthread_mutex_unlock(&tree_addrmutex); +} + +void listener_store_bykey(rad_listen_t *listener, const char *newkey) +{ + radlisten_list_t *list; + + if(!listener->with_coa) return; + + rad_assert(newkey); + + pthread_mutex_lock(&tree_keymutex); + + listener->key = talloc_typed_strdup(listener, newkey); + list = listener_findlist_bykey(newkey); + + if(list) { + radlisten_node_t *node = radlisten_node_init(list, listener); + list->tail->next = node; + list->tail = node; + } else { + list = talloc_zero(tree_key, radlisten_list_t); + radlisten_node_t *node = radlisten_node_init(list, listener); + list->head = node; + list->tail = node; + list->current = node; + rbtree_insert(tree_key, list); + } + + list->num++; + + pthread_mutex_unlock(&tree_keymutex); +} + +#endif /* WITH_COA_SINGLE_TUNNEL */ diff --git a/src/main/pair.c b/src/main/pair.c index 7537b0b7c261e..030b905157c18 100644 --- a/src/main/pair.c +++ b/src/main/pair.c @@ -773,7 +773,7 @@ void rdebug_pair_list(log_lvl_t level, REQUEST *request, VALUE_PAIR *vp, char co VERIFY_VP(vp); if (vp->da->flags.secret && request->root->suppress_secrets && (rad_debug_lvl < 3)) { - RDEBUGX(level, "%s%s = <<< secret >>>"prefix ? prefix : "", vp->da->name); + RDEBUGX(level, "%s%s = <<< secret >>>", prefix ? prefix : "", vp->da->name); continue; } diff --git a/src/main/process.c b/src/main/process.c index 7a0271e1a442e..42dbedaeec47c 100644 --- a/src/main/process.c +++ b/src/main/process.c @@ -1984,7 +1984,7 @@ static REQUEST *request_setup(TALLOC_CTX *ctx, rad_listen_t *listener, RADIUS_PA /* * Set virtual server identity */ - if (client->server) { + if (client && client->server) { request->server = client->server; } else if (listener->server) { request->server = listener->server; @@ -2086,7 +2086,12 @@ static void tcp_socket_timer(void *ctx) * open to listen for replies to requests we had * previously sent. */ - if (listener->type == RAD_LISTEN_PROXY) { + if (listener->type == RAD_LISTEN_PROXY +#ifdef WITH_COA_SINGLE_TUNNEL + || listener->with_coa +#endif + ) + { PTHREAD_MUTEX_LOCK(&proxy_mutex); if (!fr_packet_list_socket_freeze(proxy_list, listener->fd)) { @@ -2310,12 +2315,16 @@ static int insert_into_proxy_hash(REQUEST *request) PTHREAD_MUTEX_LOCK(&proxy_mutex); - proxy_listener = NULL; + /* + * proxy listener may be preset already if coa is sent through the + * reverse connection (auth+coa/auth+acct+coa) + * */ + proxy_listener = request->proxy_listener; request->num_proxied_requests = 1; request->num_proxied_responses = 0; for (tries = 0; tries < 2; tries++) { - rad_listen_t *this; + rad_listen_t *this = proxy_listener; listen_socket_t *sock; RDEBUG3("proxy: Trying to allocate ID (%d/2)", tries); @@ -2326,6 +2335,10 @@ static int insert_into_proxy_hash(REQUEST *request) if (tries > 0) continue; /* try opening new socket only once */ +#ifdef WITH_COA_SINGLE_TUNNEL + if(request->proxy_listener) break; /* not opening new socket */ +#endif + #ifdef HAVE_PTHREAD_H if (proxy_no_new_sockets) break; #endif @@ -2485,7 +2498,7 @@ static int process_proxy_reply(REQUEST *request, RADIUS_PACKET *reply) * Decode the packet if required. */ if (request->proxy_listener) { - rcode = request->proxy_listener->decode(request->proxy_listener, request); + rcode = request->proxy_listener->decode_proxy(request->proxy_listener, request); debug_packet(request, reply, true); /* @@ -2513,7 +2526,10 @@ static int process_proxy_reply(REQUEST *request, RADIUS_PACKET *reply) */ if (request->home_server && request->home_server->server) { request->server = request->home_server->server; - +#ifdef WITH_COA_SINGLE_TUNNEL + } else if (request->home_server && request->home_server->coa_server) { + request->server = request->home_server->coa_server; +#endif } else { if (request->home_pool && request->home_pool->virtual_server) { request->server = request->home_pool->virtual_server; @@ -3414,7 +3430,10 @@ static int request_proxy(REQUEST *request) /* * We're actually sending a proxied packet. Do that now. + * insert_into_proxy_hash behaves differently if request->proxy_listener is + * already set, so make sure it is not set. */ + rad_assert(request->proxy_listener == NULL); if (!request->in_proxy_hash && !insert_into_proxy_hash(request)) { RPROXY("Failed to insert request into the proxy list"); return -1; @@ -3453,7 +3472,7 @@ static int request_proxy(REQUEST *request) /* * Encode the packet before we do anything else. */ - request->proxy_listener->encode(request->proxy_listener, request); + request->proxy_listener->encode_proxy(request->proxy_listener, request); debug_packet(request, request->proxy, false); /* @@ -3473,7 +3492,7 @@ static int request_proxy(REQUEST *request) /* * And send the packet. */ - request->proxy_listener->send(request->proxy_listener, request); + request->proxy_listener->send_proxy(request->proxy_listener, request); return 1; } @@ -3549,6 +3568,11 @@ static int request_proxy_anew(REQUEST *request) home_server_update_request(home, request); + /* + * insert_into_proxy_hash behaves differently if request->proxy_listener + * is already set, so make sure it is not set. + */ + rad_assert(request->proxy_listener == NULL); if (!insert_into_proxy_hash(request)) { RPROXY("Failed to insert retransmission into the proxy list"); goto post_proxy_fail; @@ -3817,7 +3841,7 @@ static void ping_home_server(void *ctx) rad_assert(request->proxy_listener != NULL); debug_packet(request, request->proxy, false); - request->proxy_listener->send(request->proxy_listener, + request->proxy_listener->send_proxy(request->proxy_listener, request); /* @@ -4095,7 +4119,7 @@ static void proxy_wait_for_reply(REQUEST *request, int action) home->last_packet_sent = now.tv_sec; request->proxy->timestamp = now; debug_packet(request, request->proxy, false); - request->proxy_listener->send(request->proxy_listener, request); + request->proxy_listener->send_proxy(request->proxy_listener, request); break; case FR_ACTION_TIMER: @@ -4257,6 +4281,11 @@ static void request_coa_originate(REQUEST *request) REQUEST *coa; fr_ipaddr_t ipaddr; char buffer[256]; +#ifdef WITH_COA_SINGLE_TUNNEL + size_t listcount = 0; + bool ipaddrset = false; + rad_listen_t *send_listener = NULL; +#endif VERIFY_REQUEST(request); @@ -4299,10 +4328,16 @@ static void request_coa_originate(REQUEST *request) ipaddr.af = AF_INET; ipaddr.ipaddr.ip4addr.s_addr = vp->vp_ipaddr; ipaddr.prefix = 32; +#ifdef WITH_COA_SINGLE_TUNNEL + ipaddrset = true; +#endif } else if ((vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_DST_IPV6_ADDRESS, 0, TAG_ANY)) != NULL) { ipaddr.af = AF_INET6; ipaddr.ipaddr.ip6addr = vp->vp_ipv6addr; ipaddr.prefix = 128; +#ifdef WITH_COA_SINGLE_TUNNEL + ipaddrset = true; +#endif } else if ((vp = fr_pair_find_by_num(coa->proxy->vps, PW_HOME_SERVER_POOL, 0, TAG_ANY)) != NULL) { coa->home_pool = home_pool_byname(vp->vp_strvalue, HOME_TYPE_COA); @@ -4325,6 +4360,11 @@ static void request_coa_originate(REQUEST *request) /* * If all else fails, send it to the client that * originated this request. + * Do not set ipaddrset flag to be used in sigle tunnel + * coa. Otherwise we risk to send CoA not within *exactly* + * same tunnel but rather a tunnel sharing same ip. If the + * flag is not set we do send CoA within *exactly* same + * tunnel if that is supported by the listener. */ memcpy(&ipaddr, &request->packet->src_ipaddr, sizeof(ipaddr)); } @@ -4347,7 +4387,54 @@ static void request_coa_originate(REQUEST *request) if (vp) port = vp->vp_integer; coa->home_server = home_server_find(&ipaddr, port, IPPROTO_UDP); + +#ifdef WITH_COA_SINGLE_TUNNEL if (!coa->home_server) { + const char *key = NULL; + try_next: + /* + * Give one last try to send the request through the TCP auth tunnel + * 1. By IP and possibly port + */ + if(ipaddr.af && ipaddrset) { + port = 0; + vp = fr_pair_find_by_num(coa->proxy->vps, PW_PACKET_DST_PORT, 0, TAG_ANY); + if (vp) port = vp->vp_integer; + send_listener = listener_find_byaddr(&ipaddr, port, listcount ? NULL : &listcount); + } + + /* + * 2. By explicitly defined key + */ + if(send_listener) { + RDEBUG2("Found tunnel by address %s:%d", + inet_ntop(ipaddr.af, &ipaddr.ipaddr, buffer, sizeof(buffer)), port); + goto reverse; + } else { + vp = fr_pair_find_by_num(coa->proxy->vps, PW_TCP_SESSION_KEY, 0, TAG_ANY); + if(vp) key = vp->vp_strvalue; + if(key) { + send_listener = listener_find_bykey(key, listcount ? NULL : &listcount); + } + } + + if(send_listener) { + RDEBUG2("Found tunnel by key %s", key); + goto reverse; + } else if(request->listener->with_coa) { + RDEBUG2("Found tunnel by itself"); + send_listener = request->listener; + listcount = 1; /* there is no other listener to try with */ + } + + if(send_listener) { + reverse: + rad_assert(listcount > 0); + coa->home_server = ((listen_socket_t*)send_listener->data)->home; + } + } +#endif /* WITH_COA_SINGLE_TUNNEL */ + if(!coa->home_server) { RWDEBUG2("Unknown destination %s:%d for CoA request.", inet_ntop(ipaddr.af, &ipaddr.ipaddr, buffer, sizeof(buffer)), port); @@ -4409,7 +4496,21 @@ static void request_coa_originate(REQUEST *request) REXDENT(); RDEBUG2("}"); coa->server = old_server; - } else { + } else +#ifdef WITH_COA_SINGLE_TUNNEL + if(send_listener) { + char const *old_server = coa->server; + + coa->server = send_listener->server; + RDEBUG2("server %s {", coa->server); + RINDENT(); + rcode = process_pre_proxy(pre_proxy_type, coa); + REXDENT(); + RDEBUG2("}"); + coa->server = old_server; + } else +#endif + { rcode = process_pre_proxy(pre_proxy_type, coa); } switch (rcode) { @@ -4425,6 +4526,13 @@ static void request_coa_originate(REQUEST *request) break; } +#ifdef WITH_COA_SINGLE_TUNNEL + /* + * If not set here, coa->proxy_listener will be set in + * insert_into_proxy_hash function. + */ + if(send_listener) coa->proxy_listener = send_listener; +#endif /* * Source IP / port is set when the proxy socket * is chosen. @@ -4433,6 +4541,25 @@ static void request_coa_originate(REQUEST *request) coa->proxy->dst_port = coa->home_server->port; if (!insert_into_proxy_hash(coa)) { +#ifdef WITH_COA_SINGLE_TUNNEL + /* + * If several listeners share the same IP or key, we may try + * with another one up to listcount times. Each time we try to + * find next listener we expect to get the next one. Note + * however that this is done on a best-effort basis. In worst + * case the same listener will be returned if some other thread + * searches by same IP or key. I hope that is ok. + */ + if(send_listener && --listcount) { + /* + * TODO: should run virtual server fail + * section here ? + */ + send_listener = NULL; + coa->packet->vps = NULL; + goto try_next; + } +#endif radlog_request(L_PROXY, 0, coa, "Failed to insert CoA request into proxy list"); goto fail; } @@ -4460,7 +4587,7 @@ static void request_coa_originate(REQUEST *request) /* * Encode the packet before we do anything else. */ - coa->proxy_listener->encode(coa->proxy_listener, coa); + coa->proxy_listener->encode_proxy(coa->proxy_listener, coa); debug_packet(coa, coa->proxy, false); #ifdef DEBUG_STATE_MACHINE @@ -4485,7 +4612,7 @@ static void request_coa_originate(REQUEST *request) /* * And send the packet. */ - coa->proxy_listener->send(coa->proxy_listener, coa); + coa->proxy_listener->send_proxy(coa->proxy_listener, coa); } @@ -4629,7 +4756,7 @@ static void coa_retransmit(REQUEST *request) request->proxy->dst_port, request->proxy->id); - request->proxy_listener->send(request->proxy_listener, + request->proxy_listener->send_proxy(request->proxy_listener, request); } @@ -5162,6 +5289,8 @@ static void event_new_fd(rad_listen_t *this) * Add it to the list of sockets we can use. * Server sockets (i.e. auth/acct) are never * added to the packet list. + * auth/acct + coa (with_coa == true) are + * handled in 'default' case */ case RAD_LISTEN_PROXY: #ifdef WITH_TCP @@ -5210,6 +5339,26 @@ static void event_new_fd(rad_listen_t *this) fr_exit(1); } } +#ifdef WITH_COA_SINGLE_TUNNEL + /* + * In order insert_into_proxy hash function to work we + * need to make sure proxy listener socket is inserted + * into proxy_list as soon as it is created. + * We should do that for listeners that are accepted only, + * not for the listener that accepts, hence check for parrent. + */ + if(this->with_coa && this->parent) { + PTHREAD_MUTEX_LOCK(&proxy_mutex); + if(!fr_packet_list_socket_add(proxy_list, this->fd, + sock->proto, + &sock->other_ipaddr, sock->other_port, + this)) { + ERROR("Failed adding proxy socket"); + fr_exit_now(1); + } + PTHREAD_MUTEX_UNLOCK(&proxy_mutex); + } +#endif /* WITH_COA_SINGLE_TUNNEL */ #endif /* WITH_TCP */ break; } /* switch over listener types */ @@ -5274,7 +5423,12 @@ static void event_new_fd(rad_listen_t *this) /* * Tell all requests using this socket that the socket is dead. */ - if (this->type == RAD_LISTEN_PROXY) { + if (this->type == RAD_LISTEN_PROXY +#ifdef WITH_COA_SINGLE_TUNNEL + || this->with_coa +#endif + ) + { PTHREAD_MUTEX_LOCK(&proxy_mutex); if (!fr_packet_list_socket_freeze(proxy_list, this->fd)) { @@ -5379,7 +5533,13 @@ static void event_new_fd(rad_listen_t *this) INFO(" ... shutting down socket %s (%u of %u)", buffer, home->limit.num_connections, home->limit.max_connections); } + } + if (this->type == RAD_LISTEN_PROXY +#ifdef WITH_COA_SINGLE_TUNNEL + || this->with_coa +#endif + ) { PTHREAD_MUTEX_LOCK(&proxy_mutex); fr_packet_list_walk(proxy_list, this, eol_proxy_listener); @@ -5389,7 +5549,9 @@ static void event_new_fd(rad_listen_t *this) fr_exit(1); } PTHREAD_MUTEX_UNLOCK(&proxy_mutex); - } else + } + + if(this->type != RAD_LISTEN_PROXY) #endif /* WITH_PROXY */ { INFO(" ... shutting down socket %s", buffer); @@ -6021,6 +6183,8 @@ void radius_event_free(void) if (proxy_ctx) talloc_free(proxy_ctx); #endif + listen_destroy(); + TALLOC_FREE(el); if (debug_condition) talloc_free(debug_condition); diff --git a/src/main/realms.c b/src/main/realms.c index 682c58f35114d..e06fa00e5483a 100644 --- a/src/main/realms.c +++ b/src/main/realms.c @@ -72,6 +72,10 @@ static const FR_NAME_NUMBER home_server_types[] = { { "auth", HOME_TYPE_AUTH }, { "acct", HOME_TYPE_ACCT }, { "auth+acct", HOME_TYPE_AUTH_ACCT }, +#ifdef WITH_COA_SINGLE_TUNNEL + { "auth+coa", HOME_TYPE_AUTH_COA }, + { "auth+acct+coa", HOME_TYPE_AUTH_ACCT_COA }, +#endif { "coa", HOME_TYPE_COA }, { NULL, 0 } }; @@ -427,6 +431,9 @@ static CONF_PARSER home_server_coa[] = { { "mrt", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_mrt), STRINGIFY(16) }, { "mrc", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_mrc), STRINGIFY(5) }, { "mrd", FR_CONF_OFFSET(PW_TYPE_INTEGER, home_server_t, coa_mrd), STRINGIFY(30) }, +# ifdef WITH_COA_SINGLE_TUNNEL + { "virtual_server", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_NOT_EMPTY, home_server_t, coa_server), NULL }, +# endif CONF_PARSER_TERMINATOR }; #endif @@ -677,6 +684,13 @@ bool realm_home_server_add(home_server_t *home) } } +#ifdef WITH_COA_SINGLE_TUNNEL + if (home->with_coa && !home->tls) { + ERROR("Cannot enable single tunnel coa process without tls"); + return false; + } +#endif + /* * Mark it as already processed */ @@ -696,6 +710,9 @@ home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SE { home_server_t *home; CONF_SECTION *tls; +#ifdef WITH_COA_SINGLE_TUNNEL + CONF_SECTION *coa; +#endif if (!rc) rc = realm_config; /* Use the global config */ @@ -783,6 +800,16 @@ home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SE break; #ifdef WITH_COA + +# ifdef WITH_COA_SINGLE_TUNNEL + case HOME_TYPE_AUTH_ACCT_COA: + home->dual = true; + home->with_coa = true; + break; + case HOME_TYPE_AUTH_COA: + home->with_coa = true; + break; +# endif case HOME_TYPE_COA: if (home->server != NULL) { cf_log_err_cs(cs, "Home servers of type \"coa\" cannot point to a virtual server"); @@ -815,7 +842,12 @@ home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SE } if (((home->type == HOME_TYPE_AUTH) || - (home->type == HOME_TYPE_AUTH_ACCT)) && !home->ping_user_password) { + (home->type == HOME_TYPE_AUTH_ACCT) +#ifdef WITH_COA_SINGLE_TUNNEL + || (home->type == HOME_TYPE_AUTH_COA) + || (home->type == HOME_TYPE_AUTH_ACCT_COA) +#endif + ) && !home->ping_user_password) { cf_log_err_cs(cs, "You must supply a 'password' to enable status_check=request"); goto error; } @@ -879,6 +911,13 @@ home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SE } #endif +#ifdef WITH_COA_SINGLE_TUNNEL + if(!tls && home->with_coa) { + cf_log_err_cs(cs, "Cannot accept CoA from auth tunnel with no TLS"); + goto error; + } +#endif + /* * If were doing RADSEC (tls+tcp) the secret should default * to radsec, else a secret must be set. @@ -999,6 +1038,39 @@ home_server_t *home_server_afrom_cs(TALLOC_CTX *ctx, realm_config_t *rc, CONF_SE #endif } /* end of parse home server */ + /* + * Check the CoA configuration. + */ +#ifdef WITH_COA_SINGLE_TUNNEL + coa = cf_section_sub_find(cs, "coa"); +# ifndef WITH_COA + if (coa) { + cf_log_err_cs(cs, "CoA is not available in this executable"); + goto error; + } +# endif + + if (cf_pair_find(coa, "virtual_server") != NULL) { + + if (!home->coa_server) { + cf_log_err_cs(cs, "Invalid value for virtual_server in coa section"); + goto error; + } + + /* + * Try and find a 'server' section off the coa of + * the config with a name that matches the + * virtual_server. + */ + if (!rc) goto error; + + if (!cf_section_sub_find_name2(rc->cs, "server", home->coa_server)) { + cf_log_err_cs(cs, "No such server %s", home->server); + goto error; + } + } +#endif + realm_home_server_sanitize(home, cs); return home; @@ -1123,6 +1195,16 @@ static int pool_check_home_server(UNUSED realm_config_t *rc, CONF_PAIR *cp, case HOME_TYPE_ACCT: myhome.type = HOME_TYPE_AUTH_ACCT; home = rbtree_finddata(home_servers_byname, &myhome); +#ifdef WITH_COA_SINGLE_TUNNEL + if(!home) { + myhome.type = HOME_TYPE_AUTH_COA; + home = rbtree_finddata(home_servers_byname, &myhome); + } + if(!home) { + myhome.type = HOME_TYPE_AUTH_ACCT_COA; + home = rbtree_finddata(home_servers_byname, &myhome); + } +#endif if (home) { *phome = home; return 1; @@ -1414,6 +1496,16 @@ static int server_pool_add(realm_config_t *rc, case HOME_TYPE_ACCT: myhome.type = HOME_TYPE_AUTH_ACCT; home = rbtree_finddata(home_servers_byname, &myhome); +#ifdef WITH_COA_SINGLE_TUNNEL + if (!home) { + myhome.type = HOME_TYPE_AUTH_COA; + home = rbtree_finddata(home_servers_byname, &myhome); + } + if (!home) { + myhome.type = HOME_TYPE_AUTH_ACCT_COA; + home = rbtree_finddata(home_servers_byname, &myhome); + } +#endif break; default: @@ -2977,8 +3069,12 @@ int home_server_afrom_file(char const *filename) home->dynamic = true; - if (home->server || home->dual) { - fr_strerror_printf("Dynamic home_server '%s' cannot have 'server' or 'auth+acct'", p); + if (home->server || home->dual +#ifdef WITH_COA_SINGLE_TUNNEL + || home->with_coa +#endif + ) { + fr_strerror_printf("Dynamic home_server '%s' cannot have 'server' or 'auth+acct' or '... + coa'", p); talloc_free(home); goto error; } diff --git a/src/main/tls.c b/src/main/tls.c index 377deca08fb31..7d73ab13a304c 100644 --- a/src/main/tls.c +++ b/src/main/tls.c @@ -539,7 +539,6 @@ tls_session_t *tls_new_client_session(TALLOC_CTX *ctx, fr_tls_server_conf_t *con SSL_set_ex_data(ssn->ssl, FR_TLS_EX_INDEX_SSN, (void *)ssn); if (certs) SSL_set_ex_data(ssn->ssl, fr_tls_ex_index_certs, (void *)certs); - fr_nonblock(fd); SSL_set_fd(ssn->ssl, fd); ret = SSL_connect(ssn->ssl); diff --git a/src/main/tls_listen.c b/src/main/tls_listen.c index e1293d5394baa..a5093789e34fd 100644 --- a/src/main/tls_listen.c +++ b/src/main/tls_listen.c @@ -93,7 +93,7 @@ static void tls_socket_close(rad_listen_t *listener) */ } -static int CC_HINT(nonnull) tls_socket_write(rad_listen_t *listener, REQUEST *request) +static int CC_HINT(nonnull) tls_socket_write(rad_listen_t *listener, REQUEST* request, int sockfd) { uint8_t *p; ssize_t rcode; @@ -102,8 +102,8 @@ static int CC_HINT(nonnull) tls_socket_write(rad_listen_t *listener, REQUEST *re p = sock->ssn->dirty_out.data; while (p < (sock->ssn->dirty_out.data + sock->ssn->dirty_out.used)) { - RDEBUG3("Writing to socket %d", request->packet->sockfd); - rcode = write(request->packet->sockfd, p, + RDEBUG3("Writing to socket %d", sockfd); + rcode = write(sockfd, p, (sock->ssn->dirty_out.data + sock->ssn->dirty_out.used) - p); if (rcode <= 0) { RDEBUG("Error writing to TLS socket: %s", fr_syserror(errno)); @@ -247,7 +247,7 @@ static int tls_socket_recv(rad_listen_t *listener) * More ACK data to send. Do so. */ if (sock->ssn->dirty_out.used > 0) { - tls_socket_write(listener, request); + tls_socket_write(listener, request, request->packet->sockfd); PTHREAD_MUTEX_UNLOCK(&sock->mutex); return 0; } @@ -347,6 +347,9 @@ int dual_tls_recv(rad_listen_t *listener) listen_socket_t *sock = listener->data; RADCLIENT *client = sock->client; BIO *rbio; +#ifdef WITH_COA_SINGLE_TUNNEL + bool is_reply = false; +#endif if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0; @@ -404,7 +407,16 @@ int dual_tls_recv(rad_listen_t *listener) FR_STATS_INC(dsc, total_requests); fun = rad_coa_recv; break; -#endif + +# ifdef WITH_COA_SINGLE_TUNNEL + case PW_CODE_COA_ACK: + case PW_CODE_COA_NAK: + if (!listener->with_coa) goto bad_packet; + is_reply = true; + break; +# endif + +#endif /* WITH_COA */ case PW_CODE_STATUS_SERVER: if (!main_config.status_server) { @@ -426,11 +438,20 @@ int dual_tls_recv(rad_listen_t *listener) return 0; } /* switch over packet types */ - if (!request_receive(NULL, listener, packet, client, fun)) { - FR_STATS_INC(auth, total_packets_dropped); - rad_free(&packet); - return 0; - } + +#ifdef WITH_COA_SINGLE_TUNNEL + if(is_reply) { + if (!request_proxy_reply(packet)) { + rad_free(&packet); + return 0; + } + } else +#endif + if (!request_receive(NULL, listener, packet, client, fun)) { + FR_STATS_INC(auth, total_packets_dropped); + rad_free(&packet); + return 0; + } /* * Check for more application data. @@ -522,13 +543,61 @@ int dual_tls_send(rad_listen_t *listener, REQUEST *request) if (sock->ssn->dirty_out.used > 0) { dump_hex("WRITE TO SSL", sock->ssn->dirty_out.data, sock->ssn->dirty_out.used); - tls_socket_write(listener, request); + tls_socket_write(listener, request, request->packet->sockfd); } PTHREAD_MUTEX_UNLOCK(&sock->mutex); return 0; } +#ifdef WITH_COA_SINGLE_TUNNEL +/* + * Send a request packet, with the same tunnel for auth+coa/auth+acct+coa + */ +int dual_tls_send_req(rad_listen_t *listener, REQUEST *request) +{ + listen_socket_t *sock = listener->data; + + VERIFY_REQUEST(request); + + rad_assert(listener->send_proxy == dual_tls_send_req); + + if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0; + + if (request->proxy->data_len > (MAX_PACKET_LEN - 100)) { + RWARN("Packet is large, and possibly truncated - %zd vs max %d", + request->proxy->data_len, MAX_PACKET_LEN); + } + + PTHREAD_MUTEX_LOCK(&sock->mutex); + + /* + * Write the packet to the SSL buffers. + */ + sock->ssn->record_plus(&sock->ssn->clean_in, + request->proxy->data, request->proxy->data_len); + + dump_hex("TUNNELED DATA < ", sock->ssn->clean_in.data, sock->ssn->clean_in.used); + + /* + * Do SSL magic to get encrypted data. + */ + tls_handshake_send(request, sock->ssn); + + /* + * And finally write the data to the socket. + */ + if (sock->ssn->dirty_out.used > 0) { + dump_hex("WRITE TO SSL", sock->ssn->dirty_out.data, sock->ssn->dirty_out.used); + + tls_socket_write(listener, request, request->proxy->sockfd); + } + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + + return 0; +} +#endif + static int try_connect(tls_session_t *ssn) { int ret; @@ -695,6 +764,10 @@ int proxy_tls_recv(rad_listen_t *listener) RADIUS_PACKET *packet; uint8_t *data; ssize_t data_len; +#ifdef WITH_COA_SINGLE_TUNNEL + bool is_request = false; + RADCLIENT *client = sock->client; +#endif if (listener->status != RAD_LISTEN_STATUS_KNOWN) return 0; @@ -743,6 +816,18 @@ int proxy_tls_recv(rad_listen_t *listener) #endif #ifdef WITH_COA +# ifdef WITH_COA_SINGLE_TUNNEL + case PW_CODE_COA_REQUEST: + if (!listener->with_coa) goto bad_packet; + FR_STATS_INC(coa, total_requests); + is_request = true; + break; + case PW_CODE_DISCONNECT_REQUEST: + if (!listener->with_coa) goto bad_packet; + FR_STATS_INC(dsc, total_requests); + is_request = true; + break; +# endif case PW_CODE_COA_ACK: case PW_CODE_COA_NAK: case PW_CODE_DISCONNECT_ACK: @@ -751,6 +836,9 @@ int proxy_tls_recv(rad_listen_t *listener) #endif default: +#ifdef WITH_COA_SINGLE_TUNNEL + bad_packet: +#endif /* * FIXME: Update MIB for packet types? */ @@ -763,10 +851,21 @@ int proxy_tls_recv(rad_listen_t *listener) return 0; } - if (!request_proxy_reply(packet)) { - rad_free(&packet); - return 0; - } +#ifdef WITH_COA_SINGLE_TUNNEL + if(is_request) { + packet = talloc_steal(NULL, packet); + + if(!request_receive(NULL, listener, packet, client, rad_coa_recv)) { + FR_STATS_INC(auth, total_packets_dropped); + rad_free(&packet); + return 0; + } + } else +#endif + if (!request_proxy_reply(packet)) { + rad_free(&packet); + return 0; + } return 1; } @@ -788,7 +887,7 @@ int proxy_tls_send(rad_listen_t *listener, REQUEST *request) * if there's no packet, encode it here. */ if (!request->proxy->data) { - request->proxy_listener->encode(request->proxy_listener, + request->proxy_listener->encode_proxy(request->proxy_listener, request); } @@ -833,6 +932,71 @@ int proxy_tls_send(rad_listen_t *listener, REQUEST *request) return 1; } + +#ifdef WITH_COA_SINGLE_TUNNEL +int proxy_tls_send_reply(rad_listen_t *listener, REQUEST *request) +{ + int rcode; + listen_socket_t *sock = listener->data; + + VERIFY_REQUEST(request); + + rad_assert(sock->ssn->connected); + + if ((listener->status != RAD_LISTEN_STATUS_INIT && + (listener->status != RAD_LISTEN_STATUS_KNOWN))) return 0; + + /* + * Pack the VPs + */ + if (rad_encode(request->reply, request->packet, + request->client->secret) < 0) { + RERROR("Failed encoding packet: %s", fr_strerror()); + return 0; + } + + if (request->reply->data_len > (MAX_PACKET_LEN - 100)) { + RWARN("Packet is large, and possibly truncated - %zd vs max %d", + request->reply->data_len, MAX_PACKET_LEN); + } + + /* + * Sign the packet. + */ + if (rad_sign(request->reply, request->packet, + request->client->secret) < 0) { + RERROR("Failed signing packet: %s", fr_strerror()); + return 0; + } + + DEBUG3("Proxy is writing %u bytes to SSL", + (unsigned int) request->reply->data_len); + PTHREAD_MUTEX_LOCK(&sock->mutex); + rcode = SSL_write(sock->ssn->ssl, request->reply->data, + request->reply->data_len); + if (rcode < 0) { + int err; + + err = ERR_get_error(); + switch (err) { + case SSL_ERROR_NONE: + case SSL_ERROR_WANT_READ: + case SSL_ERROR_WANT_WRITE: + break; /* let someone else retry */ + + default: + tls_error_log(NULL, "Failed in proxy send"); + DEBUG("Closing TLS socket to home server"); + tls_socket_close(listener); + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + return 0; + } + } + PTHREAD_MUTEX_UNLOCK(&sock->mutex); + + return 1; +} +#endif /* WITH_COA_SINGLE_TUNNEL */ #endif /* WITH_PROXY */ #endif /* WITH_TLS */ diff --git a/src/main/unittest.c b/src/main/unittest.c index 7ae605c516824..dd784937e0149 100644 --- a/src/main/unittest.c +++ b/src/main/unittest.c @@ -55,15 +55,25 @@ char const *radiusd_version = "FreeRADIUS Version " RADIUSD_VERSION_STRING ; /* - * Static functions. + * Stub functions. */ -static void usage(int); - void listen_free(UNUSED rad_listen_t **head) { /* do nothing */ } +#ifdef WITH_COA_SINGLE_TUNNEL +void listener_store_bykey(UNUSED rad_listen_t *listener, UNUSED char const *key) +{ + /* do nothing */ +} +#endif + +/* + * Static functions. + */ +static void usage(int); + static rad_listen_t *listen_alloc(void *ctx) { diff --git a/src/main/version.c b/src/main/version.c index f1f1e878104c7..b16beb6e67738 100644 --- a/src/main/version.c +++ b/src/main/version.c @@ -347,6 +347,13 @@ void version_init_features(CONF_SECTION *cs) #endif ); + version_add_feature(cs, "coa-single-tunnel", +#ifdef WITH_COA_SINGLE_TUNNEL + true +#else + false +#endif + ); version_add_feature(cs, "control-socket", #ifdef WITH_COMMAND_SOCKET diff --git a/src/modules/rlm_detail/rlm_detail.c b/src/modules/rlm_detail/rlm_detail.c index 5d5ab846d8616..379f697a63b7f 100644 --- a/src/modules/rlm_detail/rlm_detail.c +++ b/src/modules/rlm_detail/rlm_detail.c @@ -379,7 +379,7 @@ static rlm_rcode_t CC_HINT(nonnull) detail_do(void *instance, REQUEST *request, * suppress the write. This check prevents an infinite * loop. */ - if ((request->listener->type == RAD_LISTEN_DETAIL) && + if (request->listener && (request->listener->type == RAD_LISTEN_DETAIL) && (fnmatch(((listen_detail_t *)request->listener->data)->filename, buffer, FNM_FILE_NAME | FNM_PERIOD ) == 0)) { RWDEBUG2("Suppressing infinite loop"); diff --git a/src/tests/README b/src/tests/README index 4055d5db37514..4dd370493896b 100644 --- a/src/tests/README +++ b/src/tests/README @@ -22,6 +22,10 @@ keywords/* auth/* tests of authentication methods. +radsec/* + tests of radsec, proxy to radsec server, send CoA in TCP/TLS + session opened by the NAD to the server + modules/* tests for individual modules diff --git a/src/tests/all.mk b/src/tests/all.mk index d87a22e08201e..7fb7925a5736c 100644 --- a/src/tests/all.mk +++ b/src/tests/all.mk @@ -1,4 +1,5 @@ -SUBMAKEFILES := rbmonkey.mk unit/all.mk map/all.mk xlat/all.mk keywords/all.mk auth/all.mk modules/all.mk +SUBMAKEFILES := rbmonkey.mk unit/all.mk map/all.mk xlat/all.mk keywords/all.mk \ + auth/all.mk modules/all.mk radsec/all.mk # # Include all of the autoconf definitions into the Make variable space diff --git a/src/tests/radsec/.gitignore b/src/tests/radsec/.gitignore new file mode 100644 index 0000000000000..9daa835c1e001 --- /dev/null +++ b/src/tests/radsec/.gitignore @@ -0,0 +1,6 @@ +dictionary +radiusd.conf +sites-enabled +mods-enabled +radrelay.conf +test.conf diff --git a/src/tests/radsec/1.basic-auth.reply b/src/tests/radsec/1.basic-auth.reply new file mode 100644 index 0000000000000..77aa6b1029e51 --- /dev/null +++ b/src/tests/radsec/1.basic-auth.reply @@ -0,0 +1,2 @@ +Received Access-Accept + diff --git a/src/tests/radsec/1.basic-auth.request b/src/tests/radsec/1.basic-auth.request new file mode 100644 index 0000000000000..a6ddb8e3f6aa8 --- /dev/null +++ b/src/tests/radsec/1.basic-auth.request @@ -0,0 +1,3 @@ +User-Name = "bob", +NAS-IP-Address = "1.2.3.4", +Called-Station-Id = "key0" diff --git a/src/tests/radsec/2.ipaddrudp-coa.reply b/src/tests/radsec/2.ipaddrudp-coa.reply new file mode 100644 index 0000000000000..8ea0bfdc0e3a3 --- /dev/null +++ b/src/tests/radsec/2.ipaddrudp-coa.reply @@ -0,0 +1,4 @@ +delay 2.5 +Received CoA-ACK +Acct-Session-Id = "coa-buffered-reader:accounting:coa-request" "coa-buffered-reader:pre-proxy" "proxy-tls-default:recv-coa" "proxy-originate-coa-relay:pre-proxy" "coa:recv-coa" "coa:send-coa" "proxy-originate-coa-relay:post-proxy-coa-ack" "proxy-tls-default:send-coa" "coa-buffered-reader:post-proxy"$ + diff --git a/src/tests/radsec/2.ipaddrudp-coa.request b/src/tests/radsec/2.ipaddrudp-coa.request new file mode 100644 index 0000000000000..9d3f5eb6190b8 --- /dev/null +++ b/src/tests/radsec/2.ipaddrudp-coa.request @@ -0,0 +1,3 @@ +User-Name = "IpAddress", +NAS-IP-Address = "127.0.0.1", +Called-Station-Id = "12341", diff --git a/src/tests/radsec/3.homepooludp-coa.reply b/src/tests/radsec/3.homepooludp-coa.reply new file mode 100644 index 0000000000000..ab9f0a1790709 --- /dev/null +++ b/src/tests/radsec/3.homepooludp-coa.reply @@ -0,0 +1,4 @@ +delay 2.5 +Received CoA-ACK +Acct-Session-Id = "coa-buffered-reader:accounting:coa-request" "home-originate-coa-relay:pre-proxy" "coa:recv-coa" "coa:send-coa" "home-originate-coa-relay:post-proxy-coa-ack"$ + diff --git a/src/tests/radsec/3.homepooludp-coa.request b/src/tests/radsec/3.homepooludp-coa.request new file mode 100644 index 0000000000000..e3bff094505c4 --- /dev/null +++ b/src/tests/radsec/3.homepooludp-coa.request @@ -0,0 +1,2 @@ +User-Name = "HomePoolCoA", +Called-Station-Id = "coa-nas" diff --git a/src/tests/radsec/4.homepooltls-coa.reply b/src/tests/radsec/4.homepooltls-coa.reply new file mode 100644 index 0000000000000..46668948db69f --- /dev/null +++ b/src/tests/radsec/4.homepooltls-coa.reply @@ -0,0 +1,4 @@ +delay 2.5 +Received CoA-ACK +Acct-Session-Id = "coa-buffered-reader:accounting:coa-request" "home-originate-coa-relay:pre-proxy" "proxy-tls-default:recv-coa" "proxy-originate-coa-relay:pre-proxy" "coa:recv-coa" "coa:send-coa" "proxy-originate-coa-relay:post-proxy-coa-ack" "proxy-tls-default:send-coa" "home-originate-coa-relay:post-proxy-coa-ack"$ + diff --git a/src/tests/radsec/4.homepooltls-coa.request b/src/tests/radsec/4.homepooltls-coa.request new file mode 100644 index 0000000000000..7038e25e4e073 --- /dev/null +++ b/src/tests/radsec/4.homepooltls-coa.request @@ -0,0 +1,2 @@ +User-Name = "HomePoolCoA", +Called-Station-Id = "coa-nas-tls" diff --git a/src/tests/radsec/5.singletunnel_proxy-coa.reply b/src/tests/radsec/5.singletunnel_proxy-coa.reply new file mode 100644 index 0000000000000..81a41736e9f80 --- /dev/null +++ b/src/tests/radsec/5.singletunnel_proxy-coa.reply @@ -0,0 +1,6 @@ +# We don't need delay since proxy flow will be finished +# just after final CoA home server will return response. +#delay 2.5 +Received CoA-ACK +Acct-Session-Id = "default:pre-proxy" "coa_tls:recv-coa" "proxy-originate-coa-relay:pre-proxy" "coa:recv-coa" "coa:send-coa" "proxy-originate-coa-relay:post-proxy-coa-ack" "coa_tls:send-coa" "default:post-proxy-coa-ack"$ + diff --git a/src/tests/radsec/5.singletunnel_proxy-coa.request b/src/tests/radsec/5.singletunnel_proxy-coa.request new file mode 100644 index 0000000000000..72ace4df8122a --- /dev/null +++ b/src/tests/radsec/5.singletunnel_proxy-coa.request @@ -0,0 +1,2 @@ +User-Name = "TcpSessionKey-Proxy", +Called-Station-Id = "key0" diff --git a/src/tests/radsec/6.singletunnel_originate-coa.reply b/src/tests/radsec/6.singletunnel_originate-coa.reply new file mode 100644 index 0000000000000..6a242b00e037e --- /dev/null +++ b/src/tests/radsec/6.singletunnel_originate-coa.reply @@ -0,0 +1,4 @@ +delay 2.5 +Received CoA-ACK +Acct-Session-Id = "coa-buffered-reader:accounting:coa-request" "default:pre-proxy" "coa_tls:recv-coa" "proxy-originate-coa-relay:pre-proxy" "coa:recv-coa" "coa:send-coa" "proxy-originate-coa-relay:post-proxy-coa-ack" "coa_tls:send-coa" "default:post-proxy-coa-ack" + diff --git a/src/tests/radsec/6.singletunnel_originate-coa.request b/src/tests/radsec/6.singletunnel_originate-coa.request new file mode 100644 index 0000000000000..a83873069495b --- /dev/null +++ b/src/tests/radsec/6.singletunnel_originate-coa.request @@ -0,0 +1,2 @@ +User-Name = "TcpSessionKey", +Called-Station-Id = "key0" diff --git a/src/tests/radsec/7.coareply-auth.reply b/src/tests/radsec/7.coareply-auth.reply new file mode 100644 index 0000000000000..62e680ecb581b --- /dev/null +++ b/src/tests/radsec/7.coareply-auth.reply @@ -0,0 +1,4 @@ +delay 2.5 +Received Access-Accept +Acct-Session-Id = "default:post-auth" "default:pre-proxy" "coa_tls:recv-coa" "proxy-originate-coa-relay:pre-proxy" "coa:recv-coa" "coa:send-coa" "proxy-originate-coa-relay:post-proxy-coa-ack" "coa_tls:send-coa" "default:post-proxy-coa-ack"$ + diff --git a/src/tests/radsec/7.coareply-auth.request b/src/tests/radsec/7.coareply-auth.request new file mode 100644 index 0000000000000..bd2e2b4b416a7 --- /dev/null +++ b/src/tests/radsec/7.coareply-auth.request @@ -0,0 +1,2 @@ +User-Name = "PostAuthCoA", +Called-Station-Id = "key0", diff --git a/src/tests/radsec/Makefile b/src/tests/radsec/Makefile new file mode 100644 index 0000000000000..d732b291d016b --- /dev/null +++ b/src/tests/radsec/Makefile @@ -0,0 +1,10 @@ +include ../../../Make.inc + +all: tests.radsec + @echo "All tests done" + +include all.mk + + +.PHONY: clean +clean: clean.tests.radsec \ No newline at end of file diff --git a/src/tests/radsec/README.rst b/src/tests/radsec/README.rst new file mode 100644 index 0000000000000..a016a028ecf73 --- /dev/null +++ b/src/tests/radsec/README.rst @@ -0,0 +1,103 @@ +======================= +Tests for radsec flows. +======================= + + RADIUS CoA + ┌─────────────────────────────────────────────────────────────┐ + │ │ +┌──────▼───────┐ ┌────────────────┐ ┌───────┴────────┐ +│ │ │ │ RADSEC CoA │ │ +│ radiusd │ RADIUS CoA │ radiusd ◄──────────────┤ radiusd │ +│ ◄─────────────┤ │ RADSEC Auth │ │ +│ CoA Server │ │ Proxy Server ├──────────────► Home Server │ +│ │ │ │ │ │ +└──────────────┘ └───────▲────────┘ └───────▲────────┘ + │ │ + │ RADIUS │ RADIUS + │ Auth │ CoA + ┌───────┴────────┐ ┌───────┴────────┐ + │ radclient │ │ radclient │ + └────────────────┘ └────────────────┘ + + +FreeRADIUS common configuration is located (obviously) in +src/tests/radsec/radddb directory. Specific configurations for separate radiusd +instances are located under their respective directories: config-coa, +config-proxy, config-home. + +Each test is a pair of two files ending with \*.request and \*.reply. + +To run these tests separately, make sure you run 'make test' from the root +directory beforehand. + +Request files. +============== + +\*.request file specifies attributes to be sent. + +The name of the file (the part after the dash) specifies the type of the request +to be sent. + +For example 1.basic-auth.request sends an auth request and 2.basic-coa.request +sends coa. + +* Authentication requests. +-------------------------- +Radclient sends plain RADIUS Access-Request to Proxy Server. Proxy Server then +proxies this authentication request with RADSEC to Home Server. An opened TLS +tunnel is used later to accept CoA requests from Home Server. + +* CoA requests. +--------------- +Radclient sends plain RADIUS CoA request to Home Server. Depending on the +attributes Home Server does one of the following: + +- Originates CoA request to Proxy Server with RADSEC - original flow. This is +the regular flow where Proxy Server acts as a TCP server and Home Server (as +a TCP client) first needs to establish a connection to it. + +- Originates CoA request to Proxy Server with RADSEC - 'single tunnel flow'. +This is the new flow where Proxy Server can accept CoA requests from Home Server +within the same tunnel that it has opened for Access-Request. In this case, the +Proxy Server is still a TCP client yet in terms of RADIUS protocol it acts as +a CoA Server. + +In both of these two cases, the Proxy Server forwards a CoA request to CoA +Server to complete the flow. As an example CoA Server responds with CoA-ACK, +then in turn Proxy Server responds with CoA-ACK to Home Server and the flow +completes. + +- Originates CoA request directly to CoA Server. Although this is not a RADSEC +flow, that is also good to check. + + +Reply files. +============ + +\*.reply file specify a result to be expected for the corresponding \*.request +file. + + +For each such pair of \*.request \*.reply files runtest.sh is run. + +This shell script sends a request with radclient. + +Several freeRADIUS instances process requests and add attributes to be checked. +In the end of the flow all cumulative attributes are written to the detail_test +file for later checking. + +The runtest.sh checks the result following a \*.reply file. + +After test is performed a new directory is created with name "$TEST_NAME.result" +where all intermediate files realted to the test are located, an example of the +directory structure is like follows: + +ok - status file: either ok or fail +detail_test - helper file to save attributes by freeRADIUS +2.ipaddrtls-coa.reply.tmp - reply file w/o internal commands (e.g delay) +fr-home-2.ipaddrtls-coa.log - a part of freeRADIUS logs related to the test +fr-coa-2.ipaddrtls-coa.log - the same just for radiusd CoA Server +fr-proxy-2.ipaddrtls-coa.log - the same just for radiusd Proxy Server +radclient.log - logs for radclient +result-2.ipaddrtls-coa.log - combined and aggregated radclient.log and + - detail_test to be checked against \*.reply file diff --git a/src/tests/radsec/all.mk b/src/tests/radsec/all.mk new file mode 100644 index 0000000000000..f5c4492ad37c7 --- /dev/null +++ b/src/tests/radsec/all.mk @@ -0,0 +1,150 @@ +BUILD_PATH := $(top_builddir)/build +TEST_PATH := $(top_builddir)/src/tests/radsec +BIN_PATH := $(BUILD_PATH)/bin/local +LIB_PATH := $(BUILD_PATH)/lib/.libs/ +RADDB_PATH := $(top_builddir)/raddb + +# Naming convention for ports is like follows: port--. +# Owner may be either CoA Server, Proxy Server or Home Server +port-proxy-auth = 12340 +port-proxy-coa = 12341 +port-home-auth = 12342 +port-home-coa = 12343 +port-coa = 12344 + +# Port difines for request types: auth or coa +auth-port = $(port-proxy-auth) +coa-port = $(port-home-coa) + + +# +# You can watch what it's doing by: +# +# $ VERBOSE=1 make ... args ... +# +ifeq "${VERBOSE}" "" + Q=@ +else + Q= +endif + +raddb: + ${Q}echo "Setting up raddb directory" + ${Q}cp -r $(top_builddir)/raddb $(TEST_PATH) + ${Q}rm -rf $(TEST_PATH)/raddb/sites-enabled/* # we have per server config + ${Q}echo 'detail detail_test {' >> $(TEST_PATH)/raddb/mods-enabled/detail + ${Q}echo ' filename = $${radacctdir}/detail_test' >> $(TEST_PATH)/raddb/mods-enabled/detail + ${Q}echo '}' >> $(TEST_PATH)/raddb/mods-enabled/detail + ${Q}echo 'detail detail_coa {' >> $(TEST_PATH)/raddb/mods-enabled/detail + ${Q}echo ' filename = $${radacctdir}/detail_coa' >> $(TEST_PATH)/raddb/mods-enabled/detail + ${Q}echo '}' >> $(TEST_PATH)/raddb/mods-enabled/detail + + ${Q}$(MAKE) -C $(TEST_PATH)/raddb/certs + +dictionary: + ${Q}echo "# test dictionary not install. Delete at any time." > $(TEST_PATH)/dictionary + ${Q}echo '$$INCLUDE ' $(top_builddir)/share/dictionary >> $(TEST_PATH)/dictionary + + +define TEST_CONF + ${Q}printf "Configuring radiusd $(1) -> " + ${Q}echo "# radiusd test configuration file. Do not install. Delete at any time." > $(TEST_PATH)/test-$(1).conf + ${Q}echo "libdir =" $(LIB_PATH) >> $(TEST_PATH)/test-$(1).conf + ${Q}echo "testdir =" $(TEST_PATH) >> $(TEST_PATH)/test-$(1).conf + ${Q}echo 'logdir = $${testdir}' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo 'maindir = ${TEST_PATH}/raddb/' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo 'radacctdir = $${testdir}' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo 'pidfile = $${testdir}/radiusd-$(1).pid' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo 'panic_action = "gdb -batch -x $${testdir}/panic.gdb %e %p > $${testdir}/gdb-$(1).log 2>&1; cat $${testdir}/gdb-$(1).log"' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo 'security {' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo ' allow_vulnerable_openssl = yes' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo '}' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo >> $(TEST_PATH)/test-$(1).conf + ${Q}echo 'modconfdir = $${maindir}mods-config' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo 'certdir = $${maindir}/certs' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo 'cadir = $${maindir}/certs' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo '$$INCLUDE $${testdir}/config-$(1)/main.conf' >> $(TEST_PATH)/test-$(1).conf + ${Q}echo '$$INCLUDE $${maindir}/radiusd.conf' >> $(TEST_PATH)/test-$(1).conf + ${Q}rm -f $(TEST_PATH)/gdb-$(1).log $(TEST_PATH)/fr-$(1).log +endef + +define START_SERVER + ${Q}printf "Starting $(1) server... " + ${Q}if ! $(BIN_PATH)/radiusd -Pxxxxml $(TEST_PATH)/fr-$(1).log -d $(TEST_PATH) -n test-$(1) -D $(TEST_PATH); then \ + echo "failed"; \ + echo "Last log entries were:"; \ + tail -n 20 "$(TEST_PATH)/fr-$(1).log"; \ + else \ + echo "ok"; \ + fi +endef + +define PID_SERVER + ${Q}sed 's/$${{port-proxy-auth}}/$(port-proxy-auth)/g; \ + s/$${{port-proxy-coa}}/$(port-proxy-coa)/g; \ + s/$${{port-home-auth}}/$(port-home-auth)/g; \ + s/$${{port-home-coa}}/$(port-home-coa)/g; \ + s/$${{port-coa}}/$(port-coa)/g' \ + $(TEST_PATH)/config-$(1)/main.conf.template > $(TEST_PATH)/config-$(1)/main.conf + $(call TEST_CONF,$(1)) + $(call START_SERVER,$(1)) +endef + +radiusd.pid: raddb dictionary + $(call PID_SERVER,coa) + $(call PID_SERVER,home) + $(call PID_SERVER,proxy) + +define KILL_SERVER + ${Q}if [ -f $(TEST_PATH)/radiusd-$(1).pid ]; then \ + if ! ps `cat $(TEST_PATH)/radiusd-$(1).pid` >/dev/null 2>&1; then \ + rm -f $(TEST_PATH)/radiusd-$(1).pid; \ + echo "FreeRADIUS terminated during test"; \ + echo "GDB output was:"; \ + cat "$(TEST_PATH)/gdb-$(1).log"; \ + echo "Last log entries were:"; \ + tail -n 20 $(TEST_PATH)/fr-$(1).log; \ + fi; \ + if ! kill -TERM `cat $(TEST_PATH)/radiusd-$(1).pid` >/dev/null 2>&1; then \ + echo "Cannot kill $(TEST_PATH)/radiusd-$(1).pid"; \ + fi; \ + fi + ${Q}rm -f $(TEST_PATH)/radiusd-$(1).pid $(TEST_PATH)/config-$(1)/*.conf +endef + +radiusd-proxy.kill: + $(call KILL_SERVER,proxy) +radiusd-home.kill: + $(call KILL_SERVER,home) +radiusd-coa.kill: + $(call KILL_SERVER,coa) + +radiusd.kill: radiusd-proxy.kill radiusd-home.kill radiusd-coa.kill + +# E.g: basis-auth.request -> TEST_NAME=basic-auth TYPE=auth, PORT=$(auth-port) +%.request.test: + ${Q}printf "RADSEC-TEST $@... " + ${Q}if ! TEST_NAME=$(patsubst %.request.test,%,$@) \ + TYPE=$(word 2, $(subst -, ,$(patsubst %.request.test,%,$@))) \ + PORT=$($(word 2, $(subst -, ,$(patsubst %.request.test,%,$@)))-port) \ + TEST_PATH=$(TEST_PATH) $(TEST_PATH)/runtest.sh 2>&1 > /dev/null; then \ + echo "failed"; \ + else \ + echo "ok"; \ + fi + +# kill the server (if it's running) +# start the server +# run the tests +# kill the server +#TEST_FILES = 2.basic-coa.request.test +TEST_FILES = $(sort $(addsuffix .test,$(notdir $(wildcard $(TEST_PATH)/*.request)))) +tests.radsec: radiusd.kill radiusd.pid $(TEST_FILES) + ${Q}$(MAKE) radiusd.kill + +.PHONY: clean.tests.radsec +clean.tests.radsec: radiusd.kill + ${Q}cd $(TEST_PATH) && rm -rf raddb/ detail_coa detail_test *.result *.conf dictionary *.ok *.log *.tmp + + +.PHONY: radiusd.kill radiusd-proxy.kill radiusd-home.kill radiusd-coa.kill dictionary raddb diff --git a/src/tests/radsec/config-coa/main.conf.template b/src/tests/radsec/config-coa/main.conf.template new file mode 100644 index 0000000000000..5baf4b7f1adf8 --- /dev/null +++ b/src/tests/radsec/config-coa/main.conf.template @@ -0,0 +1,37 @@ +listen { + type = coa + ipaddr = 127.0.0.1 + port = ${{port-coa}} + virtual_server = coa +} + +server coa { + + authenticate { + Auth-Type PAP { + pap + } + + Auth-Type MS-CHAP { + mschap + } + + Auth-Type EAP { + eap + } + } + + recv-coa { + update request { + &Acct-Session-Id += "coa:recv-coa" + } + } + + send-coa { + update reply { + &reply: += request:[*] + &reply:Acct-Session-Id += "coa:send-coa" + } + } +} + diff --git a/src/tests/radsec/config-home/main.conf.template b/src/tests/radsec/config-home/main.conf.template new file mode 100644 index 0000000000000..6fc52723fcaac --- /dev/null +++ b/src/tests/radsec/config-home/main.conf.template @@ -0,0 +1,323 @@ +listen { + + ipaddr = 127.0.0.1 + port = ${{port-home-auth}} + type = auth+coa + proto = tcp + + virtual_server = default + + clients = radsec + + tls { + tls_max_version="1.2" + private_key_password = whatever + private_key_file = ${certdir}/server.pem + certificate_file = ${certdir}/server.pem + ca_file = ${cadir}/ca.pem + dh_file = ${certdir}/dh + fragment_size = 8192 + ca_path = ${cadir} + cipher_list = "DEFAULT" + cipher_server_preference = no + + cache { + enable = no + lifetime = 24 # hours + } + + require_client_cert = yes + } + + # Specify the CoA retransmit parameters for CoA single tunnel + coa { + irt = 1 + mrt = 16 + mrc = 0 + mrd = 5 + } +} + +clients radsec { + client localhost { + ipaddr = 127.0.0.1 + secret = radsec + proto = tls + + limit { + max_connections = 16 + lifetime = 0 # do not close connection + idle_timeout = 0 # do not close connection even after an idle period + } + } +} + +server default { + authorize { + update control { + Originating-Realm-Key := &Called-Station-Id + Auth-Type := Accept + } + } + + authenticate { + Auth-Type PAP { + pap + } + + Auth-Type MS-CHAP { + mschap + } + + Auth-Type EAP { + eap + } + } + + post-auth { + if(User-Name && User-Name == "PostAuthCoA") { + update coa { + &Acct-Session-Id += "default:post-auth" + &Proxy-To-Originating-Realm := &Called-Station-Id + } + } + } + + pre-proxy { + update { + &proxy-request:Acct-Session-Id += "default:pre-proxy" + } + } + + post-proxy { + switch &proxy-reply:Packet-Type { + case CoA-ACK { + update proxy-reply { + &Acct-Session-Id += "default:post-proxy-coa-ack" + } + } + + case CoA-NAK { + update proxy-reply { + &Acct-Session-Id += "default:post-proxy-coa-nak" + } + } + + case Disconnect-ACK { + update proxy-reply { + &Acct-Session-Id += "default:post-proxy-disconnect-ack" + } + } + + case Disconnect-NAK { + update proxy-reply { + &Acct-Session-Id += "default:post-proxy-disconnect-nak" + } + } + + case { + fail + } + } + + # If there was no response at all + Post-Proxy-Type Fail-CoA { + ok + } + + Post-Proxy-Type Fail-Disconnect { + ok + } + + detail_test.post-proxy + } +} + +# +# CoA Relay +# +listen { + type = coa + ipaddr = 127.0.0.1 + port = ${{port-home-coa}} + virtual_server = coa +} + +server coa { + recv-coa { + + update request { + COA-Packet-Type := "%{Packet-Type}" + } + + if(&User-Name == "TcpSessionKey-Proxy") { + # Proxying CoA + update control { + &Proxy-To-Originating-Realm := &Called-Station-Id + } + } else { + # Originating CoA + detail_coa.accounting + } + } +} + +server coa-buffered-reader { + listen { + type = detail + filename = "${radacctdir}/detail_coa" + load_factor = 90 + track = yes + } + + accounting { + switch &User-Name { + case "IpAddress" { + update { + coa:Packet-DST-IP-Address := &NAS-IP-Address + coa:Packet-DST-Port:= &Called-Station-Id + } + } + case "IpAddressSingleTunnel" { + update { + coa:Packet-DST-IP-Address := &NAS-IP-Address + } + } + case "HomePoolCoA" { + update { + coa:Home-Server-Pool := &Called-Station-Id + } + } + case "TcpSessionKey"{ + update { + coa:Proxy-To-Originating-Realm := &Called-Station-Id + } + } + } + + switch &COA-Packet-Type { + case "Disconnect-Request" { + update { + # Include given attributes + &disconnect: += request:[*] + &disconnect:Packet-DST-IP-Address := &COA-Packet-DST-IP-Address + &disconnect:Packet-DST-Port := &COA-Packet-DST-Port + &disconnect:Acct-Session-Id := &COA-Acct-Session-Id + &disconnect:Acct-Delay-Time !* ANY + } + } + + case "CoA-Request" { + update { + &coa:Acct-Session-Id = "coa-buffered-reader:accounting:coa-request" + } + } + } + ok + } # accounting + + pre-proxy { + update { + &proxy-request:Acct-Session-Id += "coa-buffered-reader:pre-proxy" + } + } + + post-proxy { + update { + &proxy-reply:Acct-Session-Id += "coa-buffered-reader:post-proxy" + } + detail_test.post-proxy + } +} + +server home-originate-coa-relay { + + pre-proxy { + update { + &proxy-request:Acct-Session-Id += "home-originate-coa-relay:pre-proxy" + } + } + + post-proxy { + switch &proxy-reply:Packet-Type { + case CoA-ACK { + update { + &proxy-reply:Acct-Session-Id += "home-originate-coa-relay:post-proxy-coa-ack" + } + } + + case CoA-NAK { + update { + &proxy-reply:Acct-Session-Id += "home-originate-coa-relay:post-proxy-coa-nak" + } + } + + case Disconnect-ACK { + update { + &proxy-reply:Acct-Session-Id += "home-originate-coa-relay:post-proxy-disconnect-ack" + } + } + + case Disconnect-NAK { + update { + &proxy-reply:Acct-Session-Id += "home-originate-coa-relay:post-proxy-disconnect-nak" + } + } + + case { + fail + } + } + + # If there was no response at all + Post-Proxy-Type Fail-CoA { + ok + } + + Post-Proxy-Type Fail-Disconnect { + ok + } + + detail_test.post-proxy + } +} + +home_server coa-nas { + type = coa + ipaddr = 127.0.0.1 + port = ${{port-coa}} # A placeholder to be set in test makefile + secret = testing123 + + coa { + irt = 2 + mrt = 16 + mrc = 5 + mrd = 30 + } +} + +home_server_pool coa-nas { + type = fail-over + home_server = coa-nas + virtual_server = home-originate-coa-relay +} + +home_server coa-nas-tls { + type = coa + ipaddr = 127.0.0.1 + port = ${{port-proxy-coa}} # A placeholder to be set in test makefile + secret = testing123 + + coa { + irt = 2 + mrt = 16 + mrc = 5 + mrd = 30 + } +} + +home_server_pool coa-nas-tls { + type = fail-over + home_server = coa-nas-tls + virtual_server = home-originate-coa-relay +} diff --git a/src/tests/radsec/config-proxy/main.conf.template b/src/tests/radsec/config-proxy/main.conf.template new file mode 100644 index 0000000000000..030d72a43baa2 --- /dev/null +++ b/src/tests/radsec/config-proxy/main.conf.template @@ -0,0 +1,208 @@ +server proxy-default { + + listen { + type = auth+acct + ipaddr = 127.0.0.1 + port = ${{port-proxy-auth}} + } + + authorize { + update control { + &Proxy-To-Realm := "tls" + } + } + + authenticate { + Auth-Type PAP { + pap + } + + Auth-Type MS-CHAP { + mschap + } + + Auth-Type EAP { + eap + } + } + + pre-proxy { + update { + &Acct-Session-Id += "proxy-default:pre-proxy" + } + } + + post-proxy { + update { + &Acct-Session-Id += "proxy-default:post-proxy" + } + detail_test.recv-coa + } + + recv-coa { + update { + &Acct-Session-Id += "proxy-default:recv-coa" + } + detail_test.recv-coa + } + + send-coa { + update { + &Acct-Session-Id += "proxy-default:send-coa" + } + } +} + +server proxy-tls-default { + + listen { + type = coa + ipaddr = 127.0.0.1 + port = ${{port-proxy-coa}} + } + + recv-coa { + update { + &control:Home-Server-Pool := coa-nas + &request:Acct-Session-Id += "proxy-tls-default:recv-coa" + } + } + + send-coa { + update { + &reply:Acct-Session-Id += "proxy-tls-default:send-coa" + } + } +} + +# +# Proxy To CoA server +# +server proxy-originate-coa-relay { + pre-proxy { + update { + &proxy-request:Acct-Session-Id += "proxy-originate-coa-relay:pre-proxy" + } + } + post-proxy { + switch &proxy-reply:Packet-Type { + case CoA-ACK { + update { + &proxy-reply:Acct-Session-Id += "proxy-originate-coa-relay:post-proxy-coa-ack" + } + } + + case CoA-NAK { + update { + &proxy-reply:Acct-Session-Id += "proxy-originate-coa-relay:post-proxy-coa-nak" + } + } + + case Disconnect-ACK { + update { + &proxy-reply:Acct-Session-Id += "proxy-originate-coa-relay:post-proxy-disconnect-ack" + } + } + + case Disconnect-NAK { + update { + &proxy-reply:Acct-Session-Id += "proxy-originate-coa-relay:post-proxy-disconnect-nak" + } + } + + case { + fail + } + } + + Post-Proxy-Type Fail-CoA { + ok + } + + Post-Proxy-Type Fail-Disconnect { + ok + } + } +} + +home_server coa-nas { + type = coa + ipaddr = 127.0.0.1 + port = ${{port-coa}} # A placeholder to be set in test makefile + secret = testing123 + + coa { + irt = 2 + mrt = 16 + mrc = 5 + mrd = 30 + } +} + +home_server_pool coa-nas { + type = fail-over + home_server = coa-nas + virtual_server = proxy-originate-coa-relay +} + + +# +# Proxy To RADSEC Home server +# +server coa_tls { + recv-coa { + update control { + &request:Acct-Session-Id += "coa_tls:recv-coa" + &Home-Server-Pool := coa-nas + } + } + + # When a packet is sent, it is processed through the + # send-coa section. This applies to *both* CoA-Request and + # Disconnect-Request packets. + send-coa { + update control { + &reply:Acct-Session-Id += "coa_tls:send-coa" + } + } + + # You can use pre-proxy and post-proxy sections here, too. + # They will be processed for sending && receiving proxy packets. +} + +home_server tls { + ipaddr = 127.0.0.1 + port = ${{port-home-auth}} # A placeholder to be set in test makefile + type = auth+acct+coa + secret = radsec + proto = tcp + status_check = none + + tls { + tls_max_version="1.2" + private_key_password = whatever + private_key_file = ${certdir}/client.key + certificate_file = ${certdir}/client.pem + ca_file = ${certdir}/ca.pem + dh_file = ${certdir}/dh + random_file = /dev/urandom + fragment_size = 8192 + ca_path = ${cadir} + cipher_list = "DEFAULT" + } + + recv_coa { + virtual_server = coa_tls + } +} + +home_server_pool tls { + type = fail-over + home_server = tls + virtual_server = coa_tls +} + +realm tls { + auth_pool = tls +} + diff --git a/src/tests/radsec/runtest.sh b/src/tests/radsec/runtest.sh new file mode 100755 index 0000000000000..811f6bb65b096 --- /dev/null +++ b/src/tests/radsec/runtest.sh @@ -0,0 +1,83 @@ +#!/bin/sh +#set -x + +: ${TYPE=auth} +: ${TEST_NAME=1.basic-auth} +: ${PORT=12340} +: ${SECRET=testing123} + +cd $TEST_PATH + +BIN_PATH=../../../build/bin/local +OUTPUT=radclient.log + +RES=result-$TEST_NAME.log + +clean() { + kill $tailcoa $tailhome $tailproxy 2>&1 > /dev/null + wait $tailcoa $tailhome $tailproxy 2>&1 > /dev/null # suppress terminated messages + echo "" > detail_test + rm ./$TEST_NAME.reply.tmp fr-*-$TEST_NAME.log fail ok $RES radclient.log 2>&1 > /dev/null +} + +# Combine a list of several repeated attributes to a single attribute with delimeter: +# This: +# Acct-Session-Id = "coa-buffered-reader:accounting:coa-request" +# Acct-Session-Id = "default:send-coa" +# Become: +# Acct-Session-Id = "coa-buffered-reader:accounting:coa-request" "default:send-coa" +aggregate() { + sort -s -t= -k1,1 ./detail_test | awk -F= ' + prev!=$1 && prev{ + print prev FS val; + prev=val=""} + { + val=val?val OFS $2:$2; + prev=$1 + } + END{ + if(val){ + print prev FS val} + }' >> $RES +} + +echo "Running test: $TEST_NAME for port: $PORT type: $TYPE" + +clean + +tail -f fr-coa.log 2> /dev/null > fr-coa-$TEST_NAME.log & +tailcoa=$(echo $!) +tail -f fr-home.log 2> /dev/null > fr-home-$TEST_NAME.log & +tailhome=$(echo $!) +tail -f fr-proxy.log 2> /dev/null > fr-proxy-$TEST_NAME.log & +tailproxy=$(echo $!) + +$BIN_PATH/radclient -f $TEST_NAME.request -xF -D ./ 127.0.0.1:$PORT $TYPE $SECRET 1> $OUTPUT + +# skip comments +sed '/^\s*#/d' $TEST_NAME.reply > $TEST_NAME.reply.tmp + +# wait if needed +delay=$(grep delay $TEST_NAME.reply.tmp | awk '{print $2}') +sed '/delay/d' $TEST_NAME.reply.tmp > $TEST_NAME.reply.tmp +sleep $delay 2>&1 > /dev/null + +cat radclient.log > $RES +aggregate + +while read -r line; do + if ! grep "$line" $RES >/dev/null 2>&1; then + echo "This test failed!" >> fail + echo "Testing $TEST_NAME failed. Cannot find $line in $RES." > fail + fi +done < $TEST_NAME.reply.tmp + +if [ ! -f fail ]; then echo "This test succeded!" >> ok; fi + +mkdir $TEST_NAME.result 2>&1 > /dev/null +cp ./$TEST_NAME.reply.tmp fr-*-$TEST_NAME.log fail ok \ + $RES radclient.log detail_test $TEST_NAME.result 2>&1 > /dev/null + +clean + +test -f $TEST_NAME.result/ok # exit with the status code