package client import ( "encoding/binary" "fmt" "io" "net" "strings" "time" krbClient "github.com/jcmturner/gokrb5/v8/client" "github.com/jcmturner/gokrb5/v8/kadmin" ) // From here: https://github.com/jcmturner/gokrb5/blob/v8.4.4/v8/client/passwd.go#L51C1-L75C2 // It would be really nice if this was public, but it isn't :( func sendToKAdmin(cl *krbClient.Client, msg kadmin.Request) (r kadmin.Reply, err error) { _, kps, err := cl.Config.GetKpasswdServers(cl.Credentials.Domain(), true) if err != nil { return } b, err := msg.Marshal() if err != nil { return } var rb []byte rb, err = dialSendTCP(kps, b) if err != nil { return } err = r.Unmarshal(rb) return } // Below are from here: https://github.com/jcmturner/gokrb5/blob/master/v8/client/network.go // Likewise, it sucks that the change password API is so limited, and there is zero low-level exposure without copy-pasting code. // dialSendTCP establishes a TCP connection to a KDC. func dialSendTCP(kdcs map[int]string, b []byte) ([]byte, error) { var errs []string for i := 1; i <= len(kdcs); i++ { conn, err := net.DialTimeout("tcp", kdcs[i], 5*time.Second) if err != nil { errs = append(errs, fmt.Sprintf("error establishing connection to %s: %v", kdcs[i], err)) continue } if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { errs = append(errs, fmt.Sprintf("error setting deadline on connection to %s: %v", kdcs[i], err)) continue } // conn is guaranteed to be a TCPConn rb, err := sendTCP(conn.(*net.TCPConn), b) if err != nil { errs = append(errs, fmt.Sprintf("error sending to %s: %v", kdcs[i], err)) continue } return rb, nil } return nil, fmt.Errorf("error sending: %s", strings.Join(errs, "; ")) } // sendTCP sends bytes to connection over TCP. func sendTCP(conn *net.TCPConn, b []byte) ([]byte, error) { defer conn.Close() var r []byte // RFC 4120 7.2.2 specifies the first 4 bytes indicate the length of the message in big endian order. hb := make([]byte, 4, 4) binary.BigEndian.PutUint32(hb, uint32(len(b))) b = append(hb, b...) _, err := conn.Write(b) if err != nil { return r, fmt.Errorf("error sending to %s: %v", conn.RemoteAddr().String(), err) } sh := make([]byte, 4, 4) _, err = conn.Read(sh) if err != nil { return r, fmt.Errorf("error reading response size header: %v", err) } s := binary.BigEndian.Uint32(sh) rb := make([]byte, s, s) _, err = io.ReadFull(conn, rb) if err != nil { return r, fmt.Errorf("error reading response: %v", err) } if len(rb) < 1 { return r, fmt.Errorf("no response data from %s", conn.RemoteAddr().String()) } return rb, nil }