package checker import ( "context" "fmt" "strconv" "strings" "time" "github.com/jcmturner/gokrb5/v8/client" "github.com/jcmturner/gokrb5/v8/config" ) // runAuthProbe performs a Login (AS-REQ + preauth) and, if a targetService // is supplied, a TGS-REQ. kdcHosts maps reachable KDC hostnames to their // TCP port so we can populate the krb5 config without doing DNS again. func runAuthProbe(ctx context.Context, realm, principal, password, targetService string, kdcHosts map[string]uint16, timeout time.Duration) *AuthProbeResult { res := &AuthProbeResult{Attempted: true, Principal: principal, TargetService: targetService} username := principal if i := strings.LastIndex(principal, "@"); i >= 0 { username = principal[:i] } cfg := config.New() cfg.LibDefaults.DefaultRealm = realm cfg.LibDefaults.NoAddresses = true cfg.LibDefaults.TicketLifetime = 10 * time.Minute cfg.LibDefaults.Clockskew = 5 * time.Minute cfg.LibDefaults.UDPPreferenceLimit = 1 // force TCP cfg.LibDefaults.DefaultTktEnctypeIDs = preferredEnctypes cfg.LibDefaults.DefaultTGSEnctypeIDs = preferredEnctypes cfg.LibDefaults.PermittedEnctypeIDs = preferredEnctypes realmCfg := config.Realm{Realm: realm} for host, port := range kdcHosts { realmCfg.KDC = append(realmCfg.KDC, host+":"+strconv.Itoa(int(port))) } cfg.Realms = []config.Realm{realmCfg} cfg.DomainRealm = config.DomainRealm{strings.ToLower(realm): realm} start := time.Now() cl := client.NewWithPassword(username, realm, password, cfg, client.DisablePAFXFAST(true)) loginErr := cl.Login() res.Latency = time.Since(start) if loginErr != nil { res.Error = loginErr.Error() if code, ok := extractKRBErrorCode(loginErr); ok { res.ErrorCode = code res.ErrorName = errorcodeName(code) } return res } res.TGTAcquired = true spn := targetService if spn == "" { // Self-test: request a fresh TGT (krbtgt/REALM@REALM). spn = fmt.Sprintf("krbtgt/%s", realm) } if _, _, err := cl.GetServiceTicket(spn); err != nil { res.Error = fmt.Sprintf("TGS failed for %s: %v", spn, err) if code, ok := extractKRBErrorCode(err); ok { res.ErrorCode = code res.ErrorName = errorcodeName(code) } return res } res.TGSAcquired = true return res } // extractKRBErrorCode tries to pull a Kerberos error code out of the // wrapped errors returned by gokrb5. It's best-effort: if the code // can't be determined, ok is false. func extractKRBErrorCode(err error) (int32, bool) { if err == nil { return 0, false } // gokrb5 wraps KRBError values: their Error() string begins with "KRB Error: (N)". msg := err.Error() if _, after, ok := strings.Cut(msg, "KRB Error: ("); ok { if code, _, ok := strings.Cut(after, ")"); ok { if n, err := strconv.Atoi(code); err == nil { return int32(n), true } } } return 0, false }