]> git.ipfire.org Git - thirdparty/krb5.git/commitdiff
Defer primary KDC lookups 1300/head
authorGreg Hudson <ghudson@mit.edu>
Mon, 24 Apr 2023 22:22:40 +0000 (18:22 -0400)
committerGreg Hudson <ghudson@mit.edu>
Sun, 16 Jul 2023 22:16:09 +0000 (18:16 -0400)
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

14 files changed:
src/include/k5-trace.h
src/lib/krb5/krb/gc_via_tkt.c
src/lib/krb5/krb/get_creds.c
src/lib/krb5/krb/get_etype_info.c
src/lib/krb5/krb/get_in_tkt.c
src/lib/krb5/krb/gic_keytab.c
src/lib/krb5/krb/gic_pwd.c
src/lib/krb5/krb/in_tkt_sky.c
src/lib/krb5/krb/int-proto.h
src/lib/krb5/os/locate_kdc.c
src/lib/krb5/os/os-proto.h
src/lib/krb5/os/sendto_kdc.c
src/tests/Makefile.in
src/tests/t_sendto_kdc.py [new file with mode: 0644]

index 16a898fe7c0812f2153a852c115e63bcde31203c..5d0be63bf053e452244b1acb8ea01dbcf0304cdb 100644 (file)
@@ -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)                        \
index f8a256b2026bb64f40e3bb046e6ff69ece714737..59957273ca35a2d0889f920e9c19e16c25633be0 100644 (file)
@@ -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;
index 698c04efff8f535082a892253cf43057ee351b57..e986844a71429448e7778e70ad5da7423bfc21e9 100644 (file)
@@ -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;
 
index 1a75b9fce8a68da9cfe0eb72fdc990145df3a3ca..c3af4c32fa107a474804d0568fb2c27daa8a1037 100644 (file)
@@ -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;
                 }
index ea089f0fcc524b7a555285fc477f0948f3416f0a..4833255d9997c829b463aaf75ac48569da571928 100644 (file)
@@ -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;
 
index f9baabbf90cab753b7f2053263d5c4b43057ecb3..ba56f3b6bdc46c066ea539cce151cd63dcf22a4c 100644 (file)
@@ -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) {
index 9a3d5988a301f8c35c0135a3bb933d5637febaa0..6bf7f5bdd3297e54c251e4289510d8b39d57b09a 100644 (file)
@@ -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);
index 55c951a995a191f70b04492b2c438ec8fbb1dd28..fdf1cc7394f5f0d5d772bdd42add0eacd2c8d31d 100644 (file)
@@ -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;
index b62f9049f0186a63965dfe677ac3c552713b7ef0..5025fe8435ea7d6b9bd96e90c4e3277f8c9f6a36 100644 (file)
@@ -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
index edca5ac7eb438e30cd5cd20c950b656d6b2c7bac..7d246efac18167b43afeef469727b5c276205bae 100644 (file)
 #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);
 }
index 91d2791ce4e96d8a527c045e048919ae28062cc1..ad686a924dd1c49af75e9dc4da1b9b606d51956d 100644 (file)
@@ -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()!  */
index 0f4bf23a95496b1cdf82d444420e8ad34981bb3f..b1dc66a70fdc8c10e3c7aa74b2e0e4c3b1d09b62 100644 (file)
@@ -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:
  *
index e7cf64e086d18b1d036e45e57cebbb7844f3d6e6..1ecc9b762b7db02850e16b3f2004bfad4a6e5082 100644 (file)
@@ -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 (file)
index 0000000..8a3f8e6
--- /dev/null
@@ -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')