krb5 commit: Defer primary KDC lookups

ghudson at mit.edu ghudson at mit.edu
Sun Jul 16 18:59:59 EDT 2023


https://github.com/krb5/krb5/commit/fabbf11f457a84904a5fa251584fd660a52fa583
commit fabbf11f457a84904a5fa251584fd660a52fa583
Author: Greg Hudson <ghudson at mit.edu>
Date:   Mon Apr 24 18:22:40 2023 -0400

    Defer primary KDC lookups
    
    Add an internal variant of krb5_sendto_kdc() which records the
    answering KDC in a list.  Callers can check the list for replica KDC
    use after the success or failure of the KDC exchange is determined,
    avoiding DNS queries for the primary KDCs in many common cases and
    using fewer DNS queries in other cases.
    
    Perform the fallback in k5_get_init_creds() rather than
    krb5_get_init_creds_password().  For now we must additionally perform
    the fallback in krb5_get_init_creds_keytab() as it does not use
    k5_get_init_creds().
    
    Preserve the current signature of krb5_sendto_kdc() (it is used within
    the tree outside of libkrb5, and might be used by other software
    despite being non-public), but remove the behavior of setting
    *use_primary.
    
    ticket: 7721

 src/include/k5-trace.h            |   6 +-
 src/lib/krb5/krb/gc_via_tkt.c     |  12 +--
 src/lib/krb5/krb/get_creds.c      |  11 ++-
 src/lib/krb5/krb/get_etype_info.c |  14 ++--
 src/lib/krb5/krb/get_in_tkt.c     |  87 ++++++++++++++++------
 src/lib/krb5/krb/gic_keytab.c     |  23 +++---
 src/lib/krb5/krb/gic_pwd.c        |  73 +++---------------
 src/lib/krb5/krb/in_tkt_sky.c     |   4 +-
 src/lib/krb5/krb/int-proto.h      |   8 +-
 src/lib/krb5/os/locate_kdc.c      | 152 +++++++++++++++++++++++++++++++++++---
 src/lib/krb5/os/os-proto.h        |  24 +++++-
 src/lib/krb5/os/sendto_kdc.c      |  46 ++++++------
 src/tests/Makefile.in             |   1 +
 src/tests/t_sendto_kdc.py         |  28 +++++++
 14 files changed, 329 insertions(+), 160 deletions(-)

diff --git a/src/include/k5-trace.h b/src/include/k5-trace.h
index 16a898fe7..5d0be63bf 100644
--- a/src/include/k5-trace.h
+++ b/src/include/k5-trace.h
@@ -203,8 +203,6 @@ void krb5int_trace(krb5_context context, const char *fmt, ...);
     TRACE(c, "Attempting password change; {int} tries remaining", tries)
 #define TRACE_GIC_PWD_EXPIRED(c)                                \
     TRACE(c, "Principal expired; getting changepw ticket")
-#define TRACE_GIC_PWD_PRIMARY(c)                        \
-    TRACE(c, "Retrying AS request with primary KDC")
 
 #define TRACE_GSS_CLIENT_KEYTAB_FAIL(c, ret)                            \
     TRACE(c, "Unable to resolve default client keytab: {kerr}", ret)
@@ -249,6 +247,8 @@ void krb5int_trace(krb5_context context, const char *fmt, ...);
 #define TRACE_INIT_CREDS_PREAUTH_TRYAGAIN(c, patype, code)              \
     TRACE(c, "Recovering from KDC error {int} using preauth mech {patype}", \
           patype, (int)code)
+#define TRACE_INIT_CREDS_PRIMARY(c)                     \
+    TRACE(c, "Retrying AS request with primary KDC")
 #define TRACE_INIT_CREDS_RESTART_FAST(c)        \
     TRACE(c, "Restarting to upgrade to FAST")
 #define TRACE_INIT_CREDS_RESTART_PREAUTH_FAILED(c)                      \
@@ -387,8 +387,6 @@ void krb5int_trace(krb5_context context, const char *fmt, ...);
           rlm, (primary) ? " (primary)" : "", (tcp) ? " (tcp only)" : "")
 #define TRACE_SENDTO_KDC_K5TLS_LOAD_ERROR(c, ret)       \
     TRACE(c, "Error loading k5tls module: {kerr}", ret)
-#define TRACE_SENDTO_KDC_PRIMARY(c, primary)                            \
-    TRACE(c, "Response was{str} from primary KDC", (primary) ? "" : " not")
 #define TRACE_SENDTO_KDC_RESOLVING(c, hostname)         \
     TRACE(c, "Resolving hostname {str}", hostname)
 #define TRACE_SENDTO_KDC_RESPONSE(c, len, raddr)                        \
diff --git a/src/lib/krb5/krb/gc_via_tkt.c b/src/lib/krb5/krb/gc_via_tkt.c
index f8a256b20..59957273c 100644
--- a/src/lib/krb5/krb/gc_via_tkt.c
+++ b/src/lib/krb5/krb/gc_via_tkt.c
@@ -31,6 +31,7 @@
 
 #include "k5-int.h"
 #include "int-proto.h"
+#include "os-proto.h"
 #include "fast.h"
 
 static krb5_error_code
@@ -345,7 +346,7 @@ krb5_get_cred_via_tkt_ext(krb5_context context, krb5_creds *tkt,
     krb5_timestamp timestamp;
     krb5_int32 nonce;
     krb5_keyblock *subkey = NULL;
-    int tcp_only = 0, use_primary = 0;
+    int no_udp = 0;
     struct krb5int_fast_request_state *fast_state = NULL;
 
     request_data.data = NULL;
@@ -367,12 +368,11 @@ krb5_get_cred_via_tkt_ext(krb5_context context, krb5_creds *tkt,
         goto cleanup;
 
 send_again:
-    use_primary = 0;
-    retval = krb5_sendto_kdc(context, &request_data, &in_cred->server->realm,
-                             &response_data, &use_primary, tcp_only);
+    retval = k5_sendto_kdc(context, &request_data, &in_cred->server->realm,
+                           FALSE, no_udp, &response_data, NULL);
     if (retval == 0) {
         if (krb5_is_krb_error(&response_data)) {
-            if (!tcp_only) {
+            if (!no_udp) {
                 krb5_error *err_reply;
                 retval = decode_krb5_error(&response_data, &err_reply);
                 if (retval != 0)
@@ -382,7 +382,7 @@ send_again:
                 if (retval)
                     goto cleanup;
                 if (err_reply->error == KRB_ERR_RESPONSE_TOO_BIG) {
-                    tcp_only = 1;
+                    no_udp = 1;
                     krb5_free_error(context, err_reply);
                     krb5_free_data_contents(context, &response_data);
                     goto send_again;
diff --git a/src/lib/krb5/krb/get_creds.c b/src/lib/krb5/krb/get_creds.c
index 698c04eff..e986844a7 100644
--- a/src/lib/krb5/krb/get_creds.c
+++ b/src/lib/krb5/krb/get_creds.c
@@ -1214,23 +1214,22 @@ krb5_tkt_creds_get(krb5_context context, krb5_tkt_creds_context ctx)
     krb5_data request = empty_data(), reply = empty_data();
     krb5_data realm = empty_data();
     unsigned int flags = 0;
-    int tcp_only = 0, use_primary;
+    int no_udp = 0;
 
     for (;;) {
         /* Get the next request and realm.  Turn on TCP if necessary. */
         code = krb5_tkt_creds_step(context, ctx, &reply, &request, &realm,
                                    &flags);
-        if (code == KRB5KRB_ERR_RESPONSE_TOO_BIG && !tcp_only) {
+        if (code == KRB5KRB_ERR_RESPONSE_TOO_BIG && !no_udp) {
             TRACE_TKT_CREDS_RETRY_TCP(context);
-            tcp_only = 1;
+            no_udp = 1;
         } else if (code != 0 || !(flags & KRB5_TKT_CREDS_STEP_FLAG_CONTINUE))
             break;
         krb5_free_data_contents(context, &reply);
 
         /* Send it to a KDC for the appropriate realm. */
-        use_primary = 0;
-        code = krb5_sendto_kdc(context, &request, &realm,
-                               &reply, &use_primary, tcp_only);
+        code = k5_sendto_kdc(context, &request, &realm, FALSE, no_udp,
+                             &reply, NULL);
         if (code != 0)
             break;
 
diff --git a/src/lib/krb5/krb/get_etype_info.c b/src/lib/krb5/krb/get_etype_info.c
index 1a75b9fce..c3af4c32f 100644
--- a/src/lib/krb5/krb/get_etype_info.c
+++ b/src/lib/krb5/krb/get_etype_info.c
@@ -33,6 +33,7 @@
 #include "k5-int.h"
 #include "fast.h"
 #include "init_creds_ctx.h"
+#include "os-proto.h"
 
 /* Extract etype info from the error message pkt into icc, if it is a
  * PREAUTH_REQUIRED error.  Otherwise return the protocol error code. */
@@ -96,7 +97,7 @@ krb5_get_etype_info(krb5_context context, krb5_principal principal,
     krb5_data reply = empty_data(), req = empty_data(), realm = empty_data();
     krb5_data salt = empty_data(), s2kparams = empty_data();
     unsigned int flags;
-    int primary, tcp_only;
+    int no_udp;
     krb5_error_code ret;
 
     *enctype_out = ENCTYPE_NULL;
@@ -116,11 +117,10 @@ krb5_get_etype_info(krb5_context context, krb5_principal principal,
     }
 
     /* Send the packet (possibly once with UDP and again with TCP). */
-    tcp_only = 0;
+    no_udp = 0;
     for (;;) {
-        primary = 0;
-        ret = krb5_sendto_kdc(context, &req, &realm, &reply, &primary,
-                              tcp_only);
+        ret = k5_sendto_kdc(context, &req, &realm, FALSE, no_udp, &reply,
+                            NULL);
         if (ret)
             goto cleanup;
 
@@ -128,8 +128,8 @@ krb5_get_etype_info(krb5_context context, krb5_principal principal,
         if (krb5_is_krb_error(&reply)) {
             ret = get_from_error(context, &reply, icc);
             if (ret) {
-                if (!tcp_only && ret == KRB5KRB_ERR_RESPONSE_TOO_BIG) {
-                    tcp_only = 1;
+                if (!no_udp && ret == KRB5KRB_ERR_RESPONSE_TOO_BIG) {
+                    no_udp = 1;
                     krb5_free_data_contents(context, &reply);
                     continue;
                 }
diff --git a/src/lib/krb5/krb/get_in_tkt.c b/src/lib/krb5/krb/get_in_tkt.c
index ea089f0fc..4833255d9 100644
--- a/src/lib/krb5/krb/get_in_tkt.c
+++ b/src/lib/krb5/krb/get_in_tkt.c
@@ -544,14 +544,14 @@ krb5_init_creds_free(krb5_context context,
 
 krb5_error_code
 k5_init_creds_get(krb5_context context, krb5_init_creds_context ctx,
-                  int *use_primary)
+                  krb5_boolean use_primary, struct kdclist *kdcs)
 {
     krb5_error_code code;
     krb5_data request;
     krb5_data reply;
     krb5_data realm;
     unsigned int flags = 0;
-    int tcp_only = 0, primary = *use_primary;
+    int no_udp = 0;
 
     request.length = 0;
     request.data = NULL;
@@ -567,17 +567,16 @@ k5_init_creds_get(krb5_context context, krb5_init_creds_context ctx,
                                     &request,
                                     &realm,
                                     &flags);
-        if (code == KRB5KRB_ERR_RESPONSE_TOO_BIG && !tcp_only) {
+        if (code == KRB5KRB_ERR_RESPONSE_TOO_BIG && !no_udp) {
             TRACE_INIT_CREDS_RETRY_TCP(context);
-            tcp_only = 1;
+            no_udp = 1;
         } else if (code != 0 || !(flags & KRB5_INIT_CREDS_STEP_FLAG_CONTINUE))
             break;
 
         krb5_free_data_contents(context, &reply);
 
-        primary = *use_primary;
-        code = krb5_sendto_kdc(context, &request, &realm,
-                               &reply, &primary, tcp_only);
+        code = k5_sendto_kdc(context, &request, &realm, use_primary, no_udp,
+                             &reply, kdcs);
         if (code != 0)
             break;
 
@@ -589,7 +588,6 @@ k5_init_creds_get(krb5_context context, krb5_init_creds_context ctx,
     krb5_free_data_contents(context, &reply);
     krb5_free_data_contents(context, &realm);
 
-    *use_primary = primary;
     return code;
 }
 
@@ -598,9 +596,7 @@ krb5_error_code KRB5_CALLCONV
 krb5_init_creds_get(krb5_context context,
                     krb5_init_creds_context ctx)
 {
-    int use_primary = 0;
-
-    return k5_init_creds_get(context, ctx, &use_primary);
+    return k5_init_creds_get(context, ctx, FALSE, NULL);
 }
 
 krb5_error_code KRB5_CALLCONV
@@ -1954,13 +1950,13 @@ cleanup:
     return code;
 }
 
-krb5_error_code KRB5_CALLCONV
-k5_get_init_creds(krb5_context context, krb5_creds *creds,
-                  krb5_principal client, krb5_prompter_fct prompter,
-                  void *prompter_data, krb5_deltat start_time,
-                  const char *in_tkt_service, krb5_get_init_creds_opt *options,
-                  get_as_key_fn gak_fct, void *gak_data, int *use_primary,
-                  krb5_kdc_rep **as_reply)
+static krb5_error_code
+try_init_creds(krb5_context context, krb5_creds *creds, krb5_principal client,
+               krb5_prompter_fct prompter, void *prompter_data,
+               krb5_deltat start_time, const char *in_tkt_service,
+               krb5_get_init_creds_opt *options, get_as_key_fn gak_fct,
+               void *gak_data, krb5_boolean use_primary, struct kdclist *kdcs,
+               krb5_kdc_rep **as_reply)
 {
     krb5_error_code code;
     krb5_init_creds_context ctx = NULL;
@@ -1984,7 +1980,7 @@ k5_get_init_creds(krb5_context context, krb5_creds *creds,
             goto cleanup;
     }
 
-    code = k5_init_creds_get(context, ctx, use_primary);
+    code = k5_init_creds_get(context, ctx, use_primary, kdcs);
     if (code != 0)
         goto cleanup;
 
@@ -2003,6 +1999,56 @@ cleanup:
     return code;
 }
 
+krb5_error_code
+k5_get_init_creds(krb5_context context, krb5_creds *creds,
+                  krb5_principal client, krb5_prompter_fct prompter,
+                  void *prompter_data, krb5_deltat start_time,
+                  const char *in_tkt_service, krb5_get_init_creds_opt *options,
+                  get_as_key_fn gak_fct, void *gak_data,
+                  krb5_kdc_rep **as_reply)
+{
+    krb5_error_code ret;
+    struct kdclist *kdcs = NULL;
+    struct errinfo errsave = EMPTY_ERRINFO;
+
+    ret = k5_kdclist_create(&kdcs);
+    if (ret)
+        goto cleanup;
+
+    /* Try getting the requested ticket from any KDC. */
+    ret = try_init_creds(context, creds, client, prompter, prompter_data,
+                         start_time, in_tkt_service, options, gak_fct,
+                         gak_data, FALSE, kdcs, as_reply);
+    if (!ret)
+        goto cleanup;
+
+    /* If all of the KDCs are unavailable, or if the error was due to a user
+     * interrupt, fail. */
+    if (ret == KRB5_KDC_UNREACH || ret == KRB5_REALM_CANT_RESOLVE ||
+        ret == KRB5_LIBOS_PWDINTR || ret == KRB5_LIBOS_CANTREADPWD)
+        goto cleanup;
+
+    /* If any reply came from a replica, try again with only primary KDCs. */
+    if (k5_kdclist_any_replicas(context, kdcs)) {
+        k5_save_ctx_error(context, ret, &errsave);
+        TRACE_INIT_CREDS_PRIMARY(context);
+        ret = try_init_creds(context, creds, client, prompter, prompter_data,
+                             start_time, in_tkt_service, options, gak_fct,
+                             gak_data, TRUE, NULL, as_reply);
+        if (ret == KRB5_KDC_UNREACH || ret == KRB5_REALM_CANT_RESOLVE ||
+            ret == KRB5_REALM_UNKNOWN) {
+            /* We couldn't contact a primary KDC; return the error from the
+             * replica we were able to contact. */
+            ret = k5_restore_ctx_error(context, &errsave);
+        }
+    }
+
+cleanup:
+    k5_kdclist_free(kdcs);
+    k5_clear_error(&errsave);
+    return ret;
+}
+
 krb5_error_code
 k5_identify_realm(krb5_context context, krb5_principal client,
                   const krb5_data *subject_cert, krb5_principal *client_out)
@@ -2010,7 +2056,6 @@ k5_identify_realm(krb5_context context, krb5_principal client,
     krb5_error_code ret;
     krb5_get_init_creds_opt *opts = NULL;
     krb5_init_creds_context ctx = NULL;
-    int use_primary = 0;
 
     *client_out = NULL;
 
@@ -2030,7 +2075,7 @@ k5_identify_realm(krb5_context context, krb5_principal client,
     ctx->identify_realm = TRUE;
     ctx->subject_cert = subject_cert;
 
-    ret = k5_init_creds_get(context, ctx, &use_primary);
+    ret = k5_init_creds_get(context, ctx, FALSE, NULL);
     if (ret)
         goto cleanup;
 
diff --git a/src/lib/krb5/krb/gic_keytab.c b/src/lib/krb5/krb/gic_keytab.c
index f9baabbf9..ba56f3b6b 100644
--- a/src/lib/krb5/krb/gic_keytab.c
+++ b/src/lib/krb5/krb/gic_keytab.c
@@ -235,7 +235,8 @@ static krb5_error_code
 get_init_creds_keytab(krb5_context context, krb5_creds *creds,
                       krb5_principal client, krb5_keytab keytab,
                       krb5_deltat start_time, const char *in_tkt_service,
-                      krb5_get_init_creds_opt *options, int *use_primary)
+                      krb5_get_init_creds_opt *options,
+                      krb5_boolean use_primary, struct kdclist *kdcs)
 {
     krb5_error_code ret;
     krb5_init_creds_context ctx = NULL;
@@ -255,7 +256,7 @@ get_init_creds_keytab(krb5_context context, krb5_creds *creds,
     if (ret != 0)
         goto cleanup;
 
-    ret = k5_init_creds_get(context, ctx, use_primary);
+    ret = k5_init_creds_get(context, ctx, use_primary, kdcs);
     if (ret != 0)
         goto cleanup;
 
@@ -279,9 +280,9 @@ krb5_get_init_creds_keytab(krb5_context context,
                            krb5_get_init_creds_opt *options)
 {
     krb5_error_code ret;
-    int use_primary;
     krb5_keytab keytab;
     struct errinfo errsave = EMPTY_ERRINFO;
+    struct kdclist *kdcs = NULL;
 
     if (arg_keytab == NULL) {
         if ((ret = krb5_kt_default(context, &keytab)))
@@ -290,12 +291,14 @@ krb5_get_init_creds_keytab(krb5_context context,
         keytab = arg_keytab;
     }
 
-    use_primary = 0;
+    ret = k5_kdclist_create(&kdcs);
+    if (ret)
+        goto cleanup;
 
     /* first try: get the requested tkt from any kdc */
 
     ret = get_init_creds_keytab(context, creds, client, keytab, start_time,
-                                in_tkt_service, options, &use_primary);
+                                in_tkt_service, options, FALSE, kdcs);
 
     /* check for success */
 
@@ -310,13 +313,11 @@ krb5_get_init_creds_keytab(krb5_context context,
     /* If the reply did not come from the primary kdc, try again with
      * the primary kdc. */
 
-    if (!use_primary) {
-        use_primary = 1;
-
+    if (!k5_kdclist_any_replicas(context, kdcs)) {
         k5_save_ctx_error(context, ret, &errsave);
         ret = get_init_creds_keytab(context, creds, client, keytab,
                                     start_time, in_tkt_service, options,
-                                    &use_primary);
+                                    TRUE, NULL);
         if (ret == 0)
             goto cleanup;
 
@@ -333,6 +334,7 @@ krb5_get_init_creds_keytab(krb5_context context,
 cleanup:
     if (arg_keytab == NULL)
         krb5_kt_close(context, keytab);
+    k5_kdclist_free(kdcs);
     k5_clear_error(&errsave);
 
     return(ret);
@@ -349,7 +351,6 @@ krb5_get_in_tkt_with_keytab(krb5_context context, krb5_flags options,
     char * server = NULL;
     krb5_keytab keytab;
     krb5_principal client_princ, server_princ;
-    int use_primary = 0;
 
     retval = k5_populate_gic_opt(context, &opts, options, addrs, ktypes,
                                  pre_auth_types, creds);
@@ -370,7 +371,7 @@ krb5_get_in_tkt_with_keytab(krb5_context context, krb5_flags options,
     client_princ = creds->client;
     retval = k5_get_init_creds(context, creds, creds->client,
                                krb5_prompter_posix,  NULL, 0, server, opts,
-                               get_as_key_keytab, (void *)keytab, &use_primary,
+                               get_as_key_keytab, (void *)keytab,
                                ret_as_reply);
     krb5_free_unparsed_name( context, server);
     if (retval) {
diff --git a/src/lib/krb5/krb/gic_pwd.c b/src/lib/krb5/krb/gic_pwd.c
index 9a3d5988a..6bf7f5bdd 100644
--- a/src/lib/krb5/krb/gic_pwd.c
+++ b/src/lib/krb5/krb/gic_pwd.c
@@ -182,7 +182,6 @@ krb5_get_init_creds_password(krb5_context context,
                              krb5_get_init_creds_opt *options)
 {
     krb5_error_code ret;
-    int use_primary;
     krb5_kdc_rep *as_reply;
     int tries;
     krb5_creds chpw_creds;
@@ -192,10 +191,8 @@ krb5_get_init_creds_password(krb5_context context,
     char banner[1024], pw0array[1024], pw1array[1024];
     krb5_prompt prompt[2];
     krb5_prompt_type prompt_types[sizeof(prompt)/sizeof(prompt[0])];
-    struct errinfo errsave = EMPTY_ERRINFO;
     char *message;
 
-    use_primary = 0;
     as_reply = NULL;
     memset(&chpw_creds, 0, sizeof(chpw_creds));
     memset(&gakpw, 0, sizeof(gakpw));
@@ -205,59 +202,15 @@ krb5_get_init_creds_password(krb5_context context,
         gakpw.password = &pw0;
     }
 
-    /* first try: get the requested tkt from any kdc */
-
     ret = k5_get_init_creds(context, creds, client, prompter, data, start_time,
                             in_tkt_service, options, krb5_get_as_key_password,
-                            &gakpw, &use_primary, &as_reply);
-
-    /* check for success */
-
-    if (ret == 0)
+                            &gakpw, &as_reply);
+    if (!ret)
         goto cleanup;
 
-    /* If all the kdc's are unavailable, or if the error was due to a
-       user interrupt, fail */
-
-    if (ret == KRB5_KDC_UNREACH || ret == KRB5_REALM_CANT_RESOLVE ||
-        ret == KRB5_LIBOS_PWDINTR || ret == KRB5_LIBOS_CANTREADPWD)
-        goto cleanup;
-
-    /* If the reply did not come from the primary kdc, try again with
-     * the primary kdc. */
-
-    if (!use_primary) {
-        TRACE_GIC_PWD_PRIMARY(context);
-        use_primary = 1;
-
-        k5_save_ctx_error(context, ret, &errsave);
-        if (as_reply) {
-            krb5_free_kdc_rep( context, as_reply);
-            as_reply = NULL;
-        }
-        ret = k5_get_init_creds(context, creds, client, prompter, data,
-                                start_time, in_tkt_service, options,
-                                krb5_get_as_key_password, &gakpw, &use_primary,
-                                &as_reply);
-
-        if (ret == 0)
-            goto cleanup;
-
-        /* If the primary is unreachable, return the error from the replica we
-         * were able to contact and reset the use_primary flag. */
-        if (ret == KRB5_KDC_UNREACH || ret == KRB5_REALM_CANT_RESOLVE ||
-            ret == KRB5_REALM_UNKNOWN) {
-            ret = k5_restore_ctx_error(context, &errsave);
-            use_primary = 0;
-        }
-    }
-
-    /* at this point, we have an error from the primary.  if the error
-       is not password expired, or if it is but there's no prompter,
-       return this error */
-
-    if ((ret != KRB5KDC_ERR_KEY_EXP) ||
-        (prompter == NULL))
+    /* If the error is not password expired, or if it is but there's no
+     * prompter, return this error. */
+    if (ret != KRB5KDC_ERR_KEY_EXP || prompter == NULL)
         goto cleanup;
 
     /* historically the default has been to prompt for password change.
@@ -277,8 +230,7 @@ krb5_get_init_creds_password(krb5_context context,
         goto cleanup;
     ret = k5_get_init_creds(context, &chpw_creds, client, prompter, data,
                             start_time, "kadmin/changepw", chpw_opts,
-                            krb5_get_as_key_password, &gakpw, &use_primary,
-                            NULL);
+                            krb5_get_as_key_password, &gakpw, NULL);
     if (ret)
         goto cleanup;
 
@@ -375,15 +327,13 @@ krb5_get_init_creds_password(krb5_context context,
     if (ret)
         goto cleanup;
 
-    /* The password change was successful.  Get an initial ticket from the
-     * primary.  This is the last try.  The return from this is final. */
-
+    /* The password change was successful.  Try one last time to get an initial
+     * ticket. */
     TRACE_GIC_PWD_CHANGED(context);
     gakpw.password = &pw0;
     ret = k5_get_init_creds(context, creds, client, prompter, data,
                             start_time, in_tkt_service, options,
-                            krb5_get_as_key_password, &gakpw, &use_primary,
-                            &as_reply);
+                            krb5_get_as_key_password, &gakpw, &as_reply);
     if (ret)
         goto cleanup;
 
@@ -395,7 +345,6 @@ cleanup:
     krb5_free_cred_contents(context, &chpw_creds);
     if (as_reply)
         krb5_free_kdc_rep(context, as_reply);
-    k5_clear_error(&errsave);
 
     return(ret);
 }
@@ -432,7 +381,6 @@ krb5_get_in_tkt_with_password(krb5_context context, krb5_flags options,
     krb5_data pw;
     char * server;
     krb5_principal server_princ, client_princ;
-    int use_primary = 0;
     krb5_get_init_creds_opt *opts = NULL;
 
     memset(&gakpw, 0, sizeof(gakpw));
@@ -453,8 +401,7 @@ krb5_get_in_tkt_with_password(krb5_context context, krb5_flags options,
     client_princ = creds->client;
     retval = k5_get_init_creds(context, creds, creds->client,
                                krb5_prompter_posix, NULL, 0, server, opts,
-                               krb5_get_as_key_password, &gakpw, &use_primary,
-                               ret_as_reply);
+                               krb5_get_as_key_password, &gakpw, ret_as_reply);
     krb5_free_unparsed_name( context, server);
     krb5_get_init_creds_opt_free(context, opts);
     zapfree(gakpw.storage.data, gakpw.storage.length);
diff --git a/src/lib/krb5/krb/in_tkt_sky.c b/src/lib/krb5/krb/in_tkt_sky.c
index 55c951a99..fdf1cc739 100644
--- a/src/lib/krb5/krb/in_tkt_sky.c
+++ b/src/lib/krb5/krb/in_tkt_sky.c
@@ -75,7 +75,6 @@ krb5_get_in_tkt_with_skey(krb5_context context, krb5_flags options,
     krb5_error_code retval;
     char *server;
     krb5_principal server_princ, client_princ;
-    int use_primary = 0;
     krb5_get_init_creds_opt *opts = NULL;
 
     retval = k5_populate_gic_opt(context, &opts, options, addrs, ktypes,
@@ -105,8 +104,7 @@ krb5_get_in_tkt_with_skey(krb5_context context, krb5_flags options,
     client_princ = creds->client;
     retval = k5_get_init_creds(context, creds, creds->client,
                                krb5_prompter_posix, NULL, 0, server, opts,
-                               get_as_key_skey, (void *)key, &use_primary,
-                               ret_as_reply);
+                               get_as_key_skey, (void *)key, ret_as_reply);
     krb5_free_unparsed_name(context, server);
     if (retval)
         goto cleanup;
diff --git a/src/lib/krb5/krb/int-proto.h b/src/lib/krb5/krb/int-proto.h
index b62f9049f..5025fe843 100644
--- a/src/lib/krb5/krb/int-proto.h
+++ b/src/lib/krb5/krb/int-proto.h
@@ -28,6 +28,7 @@
 #define KRB5_INT_FUNC_PROTO__
 
 struct krb5int_fast_request_state;
+struct kdclist;
 
 typedef struct k5_response_items_st k5_response_items;
 
@@ -201,7 +202,7 @@ k5_ccselect_free_context(krb5_context context);
 
 krb5_error_code
 k5_init_creds_get(krb5_context context, krb5_init_creds_context ctx,
-                  int *use_primary);
+                  krb5_boolean use_primary, struct kdclist *kdcs);
 
 krb5_error_code
 k5_init_creds_current_time(krb5_context context, krb5_init_creds_context ctx,
@@ -286,13 +287,12 @@ k5_encrypt_keyhelper(krb5_context context, krb5_key key,
                      krb5_keyusage keyusage, const krb5_data *plain,
                      krb5_enc_data *cipher);
 
-krb5_error_code KRB5_CALLCONV
+krb5_error_code
 k5_get_init_creds(krb5_context context, krb5_creds *creds,
                   krb5_principal client, krb5_prompter_fct prompter,
                   void *prompter_data, krb5_deltat start_time,
                   const char *in_tkt_service, krb5_get_init_creds_opt *options,
-                  get_as_key_fn gak, void *gak_data, int *primary,
-                  krb5_kdc_rep **as_reply);
+                  get_as_key_fn gak, void *gak_data, krb5_kdc_rep **as_reply);
 
 /*
  * Make AS requests with the canonicalize flag set, stopping when we get a
diff --git a/src/lib/krb5/os/locate_kdc.c b/src/lib/krb5/os/locate_kdc.c
index edca5ac7e..7d246efac 100644
--- a/src/lib/krb5/os/locate_kdc.c
+++ b/src/lib/krb5/os/locate_kdc.c
@@ -38,6 +38,16 @@
 #endif
 #define DEFAULT_URI_LOOKUP TRUE
 
+struct kdclist_entry {
+    krb5_data realm;
+    struct server_entry server;
+};
+
+struct kdclist {
+    size_t count;
+    struct kdclist_entry *list;
+};
+
 static int
 maybe_use_dns (krb5_context context, const char *name, int defalt)
 {
@@ -212,6 +222,8 @@ server_list_contains(struct serverlist *list, struct server_entry *server)
     struct server_entry *ent;
 
     for (ent = list->servers; ent < list->servers + list->nservers; ent++) {
+        if (server->port != ent->port)
+            continue;
         if (server->hostname != NULL && ent->hostname != NULL &&
             strcmp(server->hostname, ent->hostname) == 0)
             return TRUE;
@@ -833,20 +845,138 @@ k5_locate_kdc(krb5_context context, const krb5_data *realm,
     return k5_locate_server(context, realm, serverlist, stype, no_udp);
 }
 
+krb5_error_code
+k5_kdclist_create(struct kdclist **kdcs_out)
+{
+    struct kdclist *kdcs;
+
+    *kdcs_out = NULL;
+    kdcs = malloc(sizeof(*kdcs));
+    if (kdcs == NULL)
+        return ENOMEM;
+    kdcs->count = 0;
+    kdcs->list = NULL;
+    *kdcs_out = kdcs;
+    return 0;
+}
+
+krb5_error_code
+k5_kdclist_add(struct kdclist *kdcs, const krb5_data *realm,
+               struct server_entry *server)
+{
+    krb5_error_code ret;
+    struct kdclist_entry *newptr, *ent;
+
+    newptr = realloc(kdcs->list, (kdcs->count + 1) * sizeof(*kdcs->list));
+    if (newptr == NULL)
+        return ENOMEM;
+    kdcs->list = newptr;
+    ent = &kdcs->list[kdcs->count];
+    ret = krb5int_copy_data_contents(NULL, realm, &ent->realm);
+    if (ret)
+        return ret;
+    /* Steal memory ownership from *server. */
+    ent->server = *server;
+    memset(server, 0, sizeof(*server));
+    kdcs->count++;
+    return 0;
+}
+
+/*
+ * If primaries is empty, mark ent as primary (the realm has no primary KDCs
+ * and therefore no KDCs are replicas).  Otherwise mark ent according to
+ * whether it is present in primaries.  Return true if ent is determined to be
+ * a replica.
+ */
+static krb5_boolean
+mark_entry(struct kdclist_entry *ent, struct serverlist *primaries)
+{
+    if (primaries->nservers == 0) {
+        ent->server.primary = 1;
+        return FALSE;
+    }
+    ent->server.primary = server_list_contains(primaries, &ent->server);
+    return !ent->server.primary;
+}
+
+/* Mark kdcs->list[start] and all entries with the same realm and transport
+ * according to primaries.  Stop and return true if a replica is found. */
+static krb5_boolean
+mark_matching_servers(struct kdclist *kdcs, size_t start,
+                      struct serverlist *primaries)
+{
+    size_t i;
+    struct kdclist_entry *ent = &kdcs->list[start];
+
+    if (mark_entry(ent, primaries))
+        return TRUE;
+    for (i = start + 1; i < kdcs->count; i++) {
+        if (kdcs->list[i].server.primary == 1)
+            continue;
+        if (kdcs->list[i].server.transport != ent->server.transport)
+            continue;
+        if (!data_eq(kdcs->list[i].realm, ent->realm))
+            continue;
+        if (mark_entry(&kdcs->list[i], primaries))
+            return TRUE;
+    }
+    return FALSE;
+}
+
+/* Return true if any entry in kdcs is a replica.  May modify the primary
+ * fields of entries in kdcs. */
 krb5_boolean
-k5_kdc_is_primary(krb5_context context, const krb5_data *realm,
-                  struct server_entry *server)
+k5_kdclist_any_replicas(krb5_context context, struct kdclist *kdcs)
 {
-    struct serverlist list;
+    size_t i;
+    struct kdclist_entry *ent;
+    struct serverlist primaries;
     krb5_boolean found;
 
-    if (server->primary != -1)
-        return server->primary;
+    /* Check if we already know that any of the KDCs is a replica. */
+    for (i = 0; i < kdcs->count; i++) {
+        if (kdcs->list[i].server.primary == 0)
+            return TRUE;
+    }
+
+    for (i = 0; i < kdcs->count; i++) {
+        ent = &kdcs->list[i];
 
-    if (locate_server(context, realm, &list, locate_service_primary_kdc,
-                      server->transport) != 0)
-        return FALSE;
-    found = server_list_contains(&list, server);
-    k5_free_serverlist(&list);
-    return found;
+        /* Skip this entry if we already know that it's not a replica. */
+        if (ent->server.primary == 1)
+            continue;
+
+        /* Look up the primary KDCs for this entry's realm and transport.  Give
+         * up and return false on error. */
+        if (locate_server(context, &ent->realm, &primaries,
+                          locate_service_primary_kdc,
+                          ent->server.transport) != 0)
+            return FALSE;
+
+        /* Using the list of primaries, determine whether this entry and any
+         * entries with the same realm and transport are replicas. */
+        found = mark_matching_servers(kdcs, i, &primaries);
+
+        k5_free_serverlist(&primaries);
+        if (found)
+            return TRUE;
+    }
+
+    return FALSE;
+}
+
+void
+k5_kdclist_free(struct kdclist *kdcs)
+{
+    size_t i;
+
+    if (kdcs == NULL)
+        return;
+    for (i = 0; i < kdcs->count; i++) {
+        free(kdcs->list[i].realm.data);
+        free(kdcs->list[i].server.hostname);
+        free(kdcs->list[i].server.uri_path);
+    }
+    free(kdcs->list);
+    free(kdcs);
 }
diff --git a/src/lib/krb5/os/os-proto.h b/src/lib/krb5/os/os-proto.h
index 91d2791ce..ad686a924 100644
--- a/src/lib/krb5/os/os-proto.h
+++ b/src/lib/krb5/os/os-proto.h
@@ -71,6 +71,8 @@ struct serverlist {
 };
 #define SERVERLIST_INIT { NULL, 0 }
 
+struct kdclist;
+
 struct remote_address {
     k5_transport transport;
     int family;
@@ -132,10 +134,23 @@ krb5_error_code k5_locate_kdc(krb5_context context, const krb5_data *realm,
                               struct serverlist *serverlist,
                               krb5_boolean get_primaries, krb5_boolean no_udp);
 
-krb5_boolean k5_kdc_is_primary(krb5_context context, const krb5_data *realm,
+void k5_free_serverlist(struct serverlist *);
+
+/* Create an object for remembering a history of KDCs contacted during an
+ * exchange. */
+krb5_error_code k5_kdclist_create(struct kdclist **kdcs_out);
+
+/* Add a server entry to kdcs.  Transfer ownership of memory from *server and
+ * zero it. */
+krb5_error_code k5_kdclist_add(struct kdclist *kdcs, const krb5_data *realm,
                                struct server_entry *server);
 
-void k5_free_serverlist(struct serverlist *);
+/* Return true if any KDC entries in kdcs are replicas, looking up realms'
+ * primary KDCs as necessary. */
+krb5_boolean k5_kdclist_any_replicas(krb5_context context,
+                                     struct kdclist *kdcs);
+
+void k5_kdclist_free(struct kdclist *kdcs);
 
 #ifdef HAVE_NETINET_IN_H
 krb5_error_code krb5_unpack_full_ipaddr(krb5_context,
@@ -189,6 +204,11 @@ krb5_error_code k5_sendto(krb5_context context, const krb5_data *message,
                                              void *),
                           void *msg_handler_data);
 
+krb5_error_code k5_sendto_kdc(krb5_context context, const krb5_data *message,
+                              const krb5_data *realm, krb5_boolean use_primary,
+                              krb5_boolean no_udp, krb5_data *reply_out,
+                              struct kdclist *hist);
+
 krb5_error_code krb5int_get_fq_local_hostname(char **);
 
 /* The io vector is *not* const here, unlike writev()!  */
diff --git a/src/lib/krb5/os/sendto_kdc.c b/src/lib/krb5/os/sendto_kdc.c
index 0f4bf23a9..b1dc66a70 100644
--- a/src/lib/krb5/os/sendto_kdc.c
+++ b/src/lib/krb5/os/sendto_kdc.c
@@ -435,13 +435,13 @@ krb5_set_kdc_recv_hook(krb5_context context, krb5_post_recv_fn recv_hook,
  */
 
 krb5_error_code
-krb5_sendto_kdc(krb5_context context, const krb5_data *message,
-                const krb5_data *realm, krb5_data *reply_out, int *use_primary,
-                int no_udp)
+k5_sendto_kdc(krb5_context context, const krb5_data *message,
+              const krb5_data *realm, krb5_boolean use_primary,
+              krb5_boolean no_udp, krb5_data *reply_out, struct kdclist *kdcs)
 {
     krb5_error_code retval, oldret, err;
     struct serverlist servers;
-    int server_used;
+    int server_used = -1;
     k5_transport_strategy strategy;
     krb5_data reply = empty_data(), *hook_message = NULL, *hook_reply = NULL;
 
@@ -460,7 +460,7 @@ krb5_sendto_kdc(krb5_context context, const krb5_data *message,
      * should probably be returned as well.
      */
 
-    TRACE_SENDTO_KDC(context, message->length, realm, *use_primary, no_udp);
+    TRACE_SENDTO_KDC(context, message->length, realm, use_primary, no_udp);
 
     if (!no_udp && context->udp_pref_limit < 0) {
         int tmp;
@@ -486,7 +486,7 @@ krb5_sendto_kdc(krb5_context context, const krb5_data *message,
     else
         strategy = UDP_LAST;
 
-    retval = k5_locate_kdc(context, realm, &servers, *use_primary, no_udp);
+    retval = k5_locate_kdc(context, realm, &servers, use_primary, no_udp);
     if (retval)
         return retval;
 
@@ -527,13 +527,9 @@ krb5_sendto_kdc(krb5_context context, const krb5_data *message,
                                         retval, realm, message, &reply,
                                         &hook_reply);
         if (oldret && !retval) {
-            /*
-             * The hook must set a reply if it overrides an error from
-             * k5_sendto().  Treat this reply as coming from the primary
-             * KDC.
-             */
+            /* The hook must set a reply if it overrides an error from
+             * k5_sendto(). */
             assert(hook_reply != NULL);
-            *use_primary = 1;
         }
     }
     if (retval)
@@ -542,18 +538,15 @@ krb5_sendto_kdc(krb5_context context, const krb5_data *message,
     if (hook_reply != NULL) {
         *reply_out = *hook_reply;
         free(hook_reply);
-    } else {
-        *reply_out = reply;
-        reply = empty_data();
+        goto cleanup;
     }
 
-    /* Set use_primary to 1 if we ended up talking to a primary when we didn't
-     * explicitly request to. */
-    if (*use_primary == 0) {
-        *use_primary = k5_kdc_is_primary(context, realm,
-                                         &servers.servers[server_used]);
-        TRACE_SENDTO_KDC_PRIMARY(context, *use_primary);
-    }
+    *reply_out = reply;
+    reply = empty_data();
+
+    /* Record which KDC we used if the caller asks. */
+    if (kdcs != NULL && server_used != -1)
+        retval = k5_kdclist_add(kdcs, realm, &servers.servers[server_used]);
 
 cleanup:
     krb5_free_data(context, hook_message);
@@ -562,6 +555,15 @@ cleanup:
     return retval;
 }
 
+krb5_error_code
+krb5_sendto_kdc(krb5_context context, const krb5_data *message,
+                const krb5_data *realm, krb5_data *reply_out, int *use_primary,
+                int no_udp)
+{
+    return k5_sendto_kdc(context, message, realm, *use_primary, no_udp,
+                         reply_out, NULL);
+}
+
 /*
  * Notes:
  *
diff --git a/src/tests/Makefile.in b/src/tests/Makefile.in
index e7cf64e08..1ecc9b762 100644
--- a/src/tests/Makefile.in
+++ b/src/tests/Makefile.in
@@ -191,6 +191,7 @@ check-pytests: responder s2p s4u2proxy unlockiter s4u2self
 	$(RUNPYTEST) $(srcdir)/t_u2u.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_kdcoptions.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_replay.py $(PYTESTFLAGS)
+	$(RUNPYTEST) $(srcdir)/t_sendto_kdc.py $(PYTESTFLAGS)
 
 clean:
 	$(RM) adata conccache etinfo forward gcred hist hooks hrealm
diff --git a/src/tests/t_sendto_kdc.py b/src/tests/t_sendto_kdc.py
new file mode 100644
index 000000000..8a3f8e62d
--- /dev/null
+++ b/src/tests/t_sendto_kdc.py
@@ -0,0 +1,28 @@
+from k5test import *
+
+realm = K5Realm(create_host=False)
+
+mark('Fallback to primary KDC')
+
+# Create a replica database and start a KDC.
+conf_rep = {'dbmodules': {'db': {'database_name': '$testdir/db.replica2'}},
+            'realms': {'$realm': {'kdc_listen': '$port9',
+                                  'kdc_tcp_listen': '$port9'}}}
+replica = realm.special_env('replica', True, kdc_conf=conf_rep)
+dumpfile = os.path.join(realm.testdir, 'dump')
+realm.run([kdb5_util, 'dump', dumpfile])
+realm.run([kdb5_util, 'load', dumpfile], env=replica)
+replica_kdc = realm.start_server([krb5kdc, '-n'], 'starting...', env=replica)
+
+# Change the password on the primary.
+realm.run([kadminl, 'cpw', '-pw', 'new', realm.user_princ])
+
+conf_fallback = {'realms': {'$realm': {'kdc': '$hostname:$port9',
+                                       'primary_kdc': '$hostname:$port0'}}}
+fallback = realm.special_env('fallback', False, krb5_conf=conf_fallback)
+msgs = ('Retrying AS request with primary KDC',)
+realm.kinit(realm.user_princ, 'new', env=fallback, expected_trace=msgs)
+
+stop_daemon(replica_kdc)
+
+success('sendto_kdc')


More information about the cvs-krb5 mailing list