kubenetworks/kubevpn

View on GitHub
pkg/ssh/ssh.go

Summary

Maintainability
F
5 days
Test Coverage
package ssh

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net"
    "net/netip"
    "net/url"
    "os"
    "path/filepath"
    "strconv"
    "strings"
    "sync"
    "time"

    "github.com/kevinburke/ssh_config"
    "github.com/pkg/errors"
    log "github.com/sirupsen/logrus"
    "github.com/spf13/pflag"
    "golang.org/x/crypto/ssh"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/schema"
    "k8s.io/cli-runtime/pkg/genericclioptions"
    "k8s.io/client-go/tools/clientcmd"
    "k8s.io/client-go/tools/clientcmd/api"
    "k8s.io/client-go/tools/clientcmd/api/latest"
    "k8s.io/client-go/util/homedir"
    "k8s.io/kubectl/pkg/cmd/util"
    "k8s.io/utils/pointer"
    "k8s.io/utils/ptr"

    "github.com/wencaiwulue/kubevpn/v2/pkg/config"
    "github.com/wencaiwulue/kubevpn/v2/pkg/daemon/rpc"
    pkgutil "github.com/wencaiwulue/kubevpn/v2/pkg/util"
)

type SshConfig struct {
    Addr             string
    User             string
    Password         string
    Keyfile          string
    Jump             string
    ConfigAlias      string
    RemoteKubeconfig string
    // GSSAPI
    GSSAPIKeytabConf string
    GSSAPIPassword   string
    GSSAPICacheFile  string
}

func (s SshConfig) Clone() SshConfig {
    return SshConfig{
        Addr:             s.Addr,
        User:             s.User,
        Password:         s.Password,
        Keyfile:          s.Keyfile,
        Jump:             s.Jump,
        ConfigAlias:      s.ConfigAlias,
        RemoteKubeconfig: s.RemoteKubeconfig,
        GSSAPIKeytabConf: s.GSSAPIKeytabConf,
        GSSAPIPassword:   s.GSSAPIPassword,
        GSSAPICacheFile:  s.GSSAPICacheFile,
    }
}

func ParseSshFromRPC(sshJump *rpc.SshJump) *SshConfig {
    if sshJump == nil {
        return &SshConfig{}
    }
    return &SshConfig{
        Addr:             sshJump.Addr,
        User:             sshJump.User,
        Password:         sshJump.Password,
        Keyfile:          sshJump.Keyfile,
        Jump:             sshJump.Jump,
        ConfigAlias:      sshJump.ConfigAlias,
        RemoteKubeconfig: sshJump.RemoteKubeconfig,
        GSSAPIKeytabConf: sshJump.GSSAPIKeytabConf,
        GSSAPIPassword:   sshJump.GSSAPIPassword,
        GSSAPICacheFile:  sshJump.GSSAPICacheFile,
    }
}

func (config *SshConfig) ToRPC() *rpc.SshJump {
    return &rpc.SshJump{
        Addr:             config.Addr,
        User:             config.User,
        Password:         config.Password,
        Keyfile:          config.Keyfile,
        Jump:             config.Jump,
        ConfigAlias:      config.ConfigAlias,
        RemoteKubeconfig: config.RemoteKubeconfig,
        GSSAPIKeytabConf: config.GSSAPIKeytabConf,
        GSSAPIPassword:   config.GSSAPIPassword,
        GSSAPICacheFile:  config.GSSAPICacheFile,
    }
}

func (config *SshConfig) IsEmpty() bool {
    return config.ConfigAlias == "" && config.Addr == "" && config.Jump == ""
}

func AddSshFlags(flags *pflag.FlagSet, sshConf *SshConfig) {
    // for ssh jumper host
    flags.StringVar(&sshConf.Addr, "ssh-addr", "", "Optional ssh jump server address to dial as <hostname>:<port>, eg: 127.0.0.1:22")
    flags.StringVar(&sshConf.User, "ssh-username", "", "Optional username for ssh jump server")
    flags.StringVar(&sshConf.Password, "ssh-password", "", "Optional password for ssh jump server")
    flags.StringVar(&sshConf.Keyfile, "ssh-keyfile", "", "Optional file with private key for SSH authentication")
    flags.StringVar(&sshConf.ConfigAlias, "ssh-alias", "", "Optional config alias with ~/.ssh/config for SSH authentication")
    flags.StringVar(&sshConf.Jump, "ssh-jump", "", "Optional bastion jump config string, eg: '--ssh-addr jumpe.naison.org --ssh-username naison --gssapi-password xxx'")
    flags.StringVar(&sshConf.GSSAPIPassword, "gssapi-password", "", "GSSAPI password")
    flags.StringVar(&sshConf.GSSAPIKeytabConf, "gssapi-keytab", "", "GSSAPI keytab file path")
    flags.StringVar(&sshConf.GSSAPICacheFile, "gssapi-cache", "", "GSSAPI cache file path, use command `kinit -c /path/to/cache USERNAME@RELAM` to generate")
    flags.StringVar(&sshConf.RemoteKubeconfig, "remote-kubeconfig", "", "Remote kubeconfig abstract path of ssh server, default is /home/$USERNAME/.kube/config")
    lookup := flags.Lookup("remote-kubeconfig")
    lookup.NoOptDefVal = "~/.kube/config"
}

// DialSshRemote https://github.com/golang/go/issues/21478
func DialSshRemote(ctx context.Context, conf *SshConfig, stopChan <-chan struct{}) (remote *ssh.Client, err error) {
    defer func() {
        if err != nil {
            if remote != nil {
                remote.Close()
            }
        }
    }()

    if conf.ConfigAlias != "" {
        remote, err = conf.AliasRecursion(ctx, stopChan)
    } else if conf.Jump != "" {
        remote, err = conf.JumpRecursion(ctx, stopChan)
    } else {
        remote, err = conf.Dial(ctx, stopChan)
    }

    // ref: https://github.com/golang/go/issues/21478
    if err == nil {
        //go func() {
        //    err2 := keepAlive(remote, conn, ctx.Done())
        //    if err2 != nil {
        //        log.Debugf("Failed to send keep-alive request: %v", err2)
        //    }
        //}()
    }
    return remote, err
}

func keepAlive(cl *ssh.Client, conn net.Conn, done <-chan struct{}) error {
    const keepAliveInterval = time.Second * 10
    t := time.NewTicker(keepAliveInterval)
    defer t.Stop()
    for {
        select {
        case <-t.C:
            _, _, err := cl.SendRequest("keepalive@golang.org", true, nil)
            if err != nil && err != io.EOF {
                return errors.Wrap(err, "failed to send keep alive")
            }
        case <-done:
            return nil
        }
    }
}

func (config SshConfig) GetAuth() ([]ssh.AuthMethod, error) {
    host, _, _ := net.SplitHostPort(config.Addr)
    var auth []ssh.AuthMethod
    var c Krb5InitiatorClient
    var err error
    var krb5Conf = GetKrb5Path()
    if config.Password != "" {
        auth = append(auth, ssh.Password(config.Password))
    } else if config.GSSAPIPassword != "" {
        c, err = NewKrb5InitiatorClientWithPassword(config.User, config.GSSAPIPassword, krb5Conf)
        if err != nil {
            return nil, err
        }
        auth = append(auth, ssh.GSSAPIWithMICAuthMethod(&c, host))
    } else if config.GSSAPIKeytabConf != "" {
        c, err = NewKrb5InitiatorClientWithKeytab(config.User, krb5Conf, config.GSSAPIKeytabConf)
        if err != nil {
            return nil, err
        }
    } else if config.GSSAPICacheFile != "" {
        c, err = NewKrb5InitiatorClientWithCache(krb5Conf, config.GSSAPICacheFile)
        if err != nil {
            return nil, err
        }
        auth = append(auth, ssh.GSSAPIWithMICAuthMethod(&c, host))
    } else {
        if config.Keyfile == "" {
            config.Keyfile = filepath.Join(homedir.HomeDir(), ".ssh", "id_rsa")
        }
        var keyFile ssh.AuthMethod
        keyFile, err = publicKeyFile(config.Keyfile)
        if err != nil {
            return nil, err
        }
        auth = append(auth, keyFile)
    }
    return auth, nil
}

func RemoteRun(client *ssh.Client, cmd string, env map[string]string) (output []byte, errOut []byte, err error) {
    var session *ssh.Session
    session, err = client.NewSession()
    if err != nil {
        return
    }
    defer session.Close()
    for k, v := range env {
        // /etc/ssh/sshd_config
        // AcceptEnv DEBIAN_FRONTEND
        if err = session.Setenv(k, v); err != nil {
            log.Warn(err)
            err = nil
        }
    }
    var out bytes.Buffer
    var er bytes.Buffer
    session.Stdout = &out
    session.Stderr = &er
    err = session.Run(cmd)
    return out.Bytes(), er.Bytes(), err
}

func publicKeyFile(file string) (ssh.AuthMethod, error) {
    var err error
    if len(file) != 0 && file[0] == '~' {
        file = filepath.Join(homedir.HomeDir(), file[1:])
    }
    file, err = filepath.Abs(file)
    if err != nil {
        err = errors.Wrap(err, fmt.Sprintf("Cannot read SSH public key file %s", file))
        return nil, err
    }
    buffer, err := os.ReadFile(file)
    if err != nil {
        err = errors.Wrap(err, fmt.Sprintf("Cannot read SSH public key file %s", file))
        return nil, err
    }

    key, err := ssh.ParsePrivateKey(buffer)
    if err != nil {
        err = errors.Wrap(err, fmt.Sprintf("Cannot parse SSH public key file %s", file))
        return nil, err
    }
    return ssh.PublicKeys(key), nil
}

func copyStream(ctx context.Context, local net.Conn, remote net.Conn) {
    chDone := make(chan bool, 2)

    // start remote -> local data transfer
    go func() {
        buf := config.LPool.Get().([]byte)[:]
        defer config.LPool.Put(buf[:])
        _, err := io.CopyBuffer(local, remote, buf)
        if err != nil && !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.EOF) {
            log.Debugf("Failed to copy remote -> local: %s", err)
        }
        select {
        case chDone <- true:
        default:
        }
    }()

    // start local -> remote data transfer
    go func() {
        buf := config.LPool.Get().([]byte)[:]
        defer config.LPool.Put(buf[:])
        _, err := io.CopyBuffer(remote, local, buf)
        if err != nil && !errors.Is(err, net.ErrClosed) && !errors.Is(err, io.EOF) {
            log.Debugf("Failed to copy local -> remote: %s", err)
        }
        select {
        case chDone <- true:
        default:
        }
    }()

    select {
    case <-chDone:
        return
    case <-ctx.Done():
        return
    }
}

func (config SshConfig) AliasRecursion(ctx context.Context, stopChan <-chan struct{}) (client *ssh.Client, err error) {
    var name = config.ConfigAlias
    var jumper = "ProxyJump"
    var bastionList = []SshConfig{GetBastion(name, config)}
    for {
        value := confList.Get(name, jumper)
        if value != "" {
            bastionList = append(bastionList, GetBastion(value, config))
            name = value
            continue
        }
        break
    }
    for i := len(bastionList) - 1; i >= 0; i-- {
        if client == nil {
            client, err = bastionList[i].Dial(ctx, stopChan)
            if err != nil {
                return
            }
        } else {
            client, err = JumpTo(ctx, client, bastionList[i], stopChan)
            if err != nil {
                return
            }
        }
    }
    return
}

func (config SshConfig) JumpRecursion(ctx context.Context, stopChan <-chan struct{}) (client *ssh.Client, err error) {
    flags := pflag.NewFlagSet("", pflag.ContinueOnError)
    var sshConf = &SshConfig{}
    AddSshFlags(flags, sshConf)
    err = flags.Parse(strings.Split(config.Jump, " "))
    if err != nil {
        return nil, err
    }
    var baseClient *ssh.Client
    baseClient, err = DialSshRemote(ctx, sshConf, stopChan)
    if err != nil {
        return nil, err
    }

    var bastionList []SshConfig
    if config.ConfigAlias != "" {
        var name = config.ConfigAlias
        var jumper = "ProxyJump"
        bastionList = append(bastionList, GetBastion(name, config))
        for {
            value := confList.Get(name, jumper)
            if value != "" {
                bastionList = append(bastionList, GetBastion(value, config))
                name = value
                continue
            }
            break
        }
    }
    if config.Addr != "" {
        bastionList = append(bastionList, config)
    }

    for _, sshConfig := range bastionList {
        client, err = JumpTo(ctx, baseClient, sshConfig, stopChan)
        if err != nil {
            return
        }
    }
    return
}

func GetBastion(name string, defaultValue SshConfig) SshConfig {
    var host, port string
    config := SshConfig{
        ConfigAlias: name,
    }
    var propertyList = []string{"ProxyJump", "Hostname", "User", "Port", "IdentityFile"}
    for i, s := range propertyList {
        value := confList.Get(name, s)
        switch i {
        case 0:

        case 1:
            host = value
        case 2:
            config.User = value
        case 3:
            if port = value; port == "" {
                port = strconv.Itoa(22)
            }
        case 4:
            if value == "" {
                config.Keyfile = defaultValue.Keyfile
                config.Password = defaultValue.Password
                config.GSSAPIKeytabConf = defaultValue.GSSAPIKeytabConf
                config.GSSAPIPassword = defaultValue.GSSAPIPassword
                config.GSSAPICacheFile = defaultValue.GSSAPICacheFile
            } else {
                config.Keyfile = value
            }
        }
    }
    config.Addr = net.JoinHostPort(host, port)
    return config
}

func (config SshConfig) Dial(ctx context.Context, stopChan <-chan struct{}) (client *ssh.Client, err error) {
    if _, _, err = net.SplitHostPort(config.Addr); err != nil {
        // use default ssh port 22
        config.Addr = net.JoinHostPort(config.Addr, "22")
        err = nil
    }
    // connect to the bastion host
    authMethod, err := config.GetAuth()
    if err != nil {
        return nil, err
    }
    d := net.Dialer{Timeout: time.Second * 10}
    conn, err := d.DialContext(ctx, "tcp", config.Addr)
    if err != nil {
        return nil, err
    }
    go func() {
        if stopChan != nil {
            <-stopChan
            conn.Close()
            if client != nil {
                client.Close()
            }
        }
    }()
    defer func() {
        if err != nil {
            if conn != nil {
                conn.Close()
            }
            if client != nil {
                client.Close()
            }
        }
    }()
    c, chans, reqs, err := ssh.NewClientConn(conn, config.Addr, &ssh.ClientConfig{
        User:            config.User,
        Auth:            authMethod,
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        BannerCallback:  ssh.BannerDisplayStderr(),
        Timeout:         time.Second * 10,
    })
    if err != nil {
        return nil, err
    }
    return ssh.NewClient(c, chans, reqs), nil
}

func JumpTo(ctx context.Context, bClient *ssh.Client, to SshConfig, stopChan <-chan struct{}) (client *ssh.Client, err error) {
    if _, _, err = net.SplitHostPort(to.Addr); err != nil {
        // use default ssh port 22
        to.Addr = net.JoinHostPort(to.Addr, "22")
        err = nil
    }

    var authMethod []ssh.AuthMethod
    authMethod, err = to.GetAuth()
    if err != nil {
        return nil, err
    }
    // Dial a connection to the service host, from the bastion
    var conn net.Conn
    conn, err = bClient.DialContext(ctx, "tcp", to.Addr)
    if err != nil {
        return
    }
    go func() {
        if stopChan != nil {
            <-stopChan
            conn.Close()
            if client != nil {
                client.Close()
            }
            bClient.Close()
        }
    }()
    defer func() {
        if err != nil {
            if client != nil {
                client.Close()
            }
            if conn != nil {
                conn.Close()
            }
        }
    }()
    var ncc ssh.Conn
    var chans <-chan ssh.NewChannel
    var reqs <-chan *ssh.Request
    ncc, chans, reqs, err = ssh.NewClientConn(conn, to.Addr, &ssh.ClientConfig{
        User:            to.User,
        Auth:            authMethod,
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
        BannerCallback:  ssh.BannerDisplayStderr(),
        Timeout:         time.Second * 10,
    })
    if err != nil {
        return
    }

    client = ssh.NewClient(ncc, chans, reqs)
    return
}

type conf []*ssh_config.Config

func (c conf) Get(alias string, key string) string {
    for _, s := range c {
        if v, err := s.Get(alias, key); err == nil {
            return v
        }
    }
    return ssh_config.Get(alias, key)
}

var once sync.Once

var confList conf

func init() {
    once.Do(func() {
        strings := []string{
            filepath.Join(homedir.HomeDir(), ".ssh", "config"),
            filepath.Join("/", "etc", "ssh", "ssh_config"),
        }
        for _, s := range strings {
            file, err := os.ReadFile(s)
            if err != nil {
                continue
            }
            cfg, err := ssh_config.DecodeBytes(file)
            if err != nil {
                continue
            }
            confList = append(confList, cfg)
        }
    })
}

type sshClient struct {
    cancel context.CancelFunc
    *ssh.Client
}

func (c *sshClient) Close() error {
    c.cancel()
    return c.Client.Close()
}

func PortMapUntil(ctx context.Context, conf *SshConfig, remote, local netip.AddrPort) error {
    // Listen on remote server port
    var lc net.ListenConfig
    localListen, e := lc.Listen(ctx, "tcp", local.String())
    if e != nil {
        return e
    }
    log.Debugf("SSH listening on local %s forward to %s", local.String(), remote.String())

    go func() {
        defer localListen.Close()

        var sshClientChan = make(chan *sshClient, 1000*1000)

        var getRemoteConnFunc = func(connCtx context.Context) (conn net.Conn, err error) {
            select {
            case cli, ok := <-sshClientChan:
                if !ok {
                    return nil, errors.New("ssh client chan closed")
                }
                ctx1, cancelFunc1 := context.WithTimeout(ctx, time.Second*10)
                defer cancelFunc1()
                conn, err = cli.DialContext(ctx1, "tcp", remote.String())
                if err != nil {
                    log.Debugf("Failed to dial remote address %s: %s", remote.String(), err)
                    cli.Close()
                    return nil, err
                }
                write := pkgutil.SafeWrite(sshClientChan, cli)
                if !write {
                    go func() {
                        <-connCtx.Done()
                        cli.Close()
                    }()
                }
                return conn, nil
            default:
                ctx1, cancelFunc1 := context.WithCancel(ctx)
                defer func() {
                    if err != nil {
                        cancelFunc1()
                    }
                }()
                ctx2, cancelFunc2 := context.WithTimeout(ctx, time.Second*10)
                defer cancelFunc2()
                var client *ssh.Client
                client, err = DialSshRemote(ctx2, conf, ctx1.Done())
                if err != nil {
                    marshal, _ := json.Marshal(conf)
                    log.Debugf("Failed to dial remote ssh server %v: %v", string(marshal), err)
                    return nil, err
                }
                ctx3, cancelFunc3 := context.WithTimeout(ctx, time.Second*10)
                defer cancelFunc3()
                conn, err = client.DialContext(ctx3, "tcp", remote.String())
                if err != nil {
                    log.Debugf("Failed to dial remote addr: %s: %v", remote.String(), err)
                    client.Close()
                    return nil, err
                }
                cli := &sshClient{cancel: cancelFunc1, Client: client}
                write := pkgutil.SafeWrite(sshClientChan, cli)
                if !write {
                    go func() {
                        <-connCtx.Done()
                        cli.Close()
                    }()
                }
                return conn, nil
            }
        }

        for ctx.Err() == nil {
            localConn, err1 := localListen.Accept()
            if err1 != nil {
                log.Debugf("Failed to accept ssh conn: %v", err1)
                continue
            }
            go func() {
                defer localConn.Close()
                cCtx, cancelFunc := context.WithCancel(ctx)
                defer cancelFunc()

                var remoteConn net.Conn
                var err error
                for i := 0; i < 5; i++ {
                    remoteConn, err = getRemoteConnFunc(cCtx)
                    if err == nil {
                        break
                    }
                }
                if err != nil {
                    log.Debugf("Failed to get remote conn: %v", err)
                    return
                }

                defer remoteConn.Close()
                copyStream(cCtx, localConn, remoteConn)
            }()
        }
    }()
    return nil
}

func SshJump(ctx context.Context, conf *SshConfig, flags *pflag.FlagSet, print bool) (path string, err error) {
    if conf.Addr == "" && conf.ConfigAlias == "" {
        if flags != nil {
            lookup := flags.Lookup("kubeconfig")
            if lookup != nil {
                if lookup.Value != nil && lookup.Value.String() != "" {
                    path = lookup.Value.String()
                } else if lookup.DefValue != "" {
                    path = lookup.DefValue
                } else {
                    path = lookup.NoOptDefVal
                }
            }
        }
        return
    }
    defer func() {
        if er := recover(); er != nil {
            err = er.(error)
        }
    }()

    // pre-check network ip connect
    var cli *ssh.Client
    cli, err = DialSshRemote(ctx, conf, ctx.Done())
    if err != nil {
        return
    }
    defer cli.Close()

    configFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()

    if conf.RemoteKubeconfig != "" || (flags != nil && flags.Changed("remote-kubeconfig")) {
        var stdout []byte
        var stderr []byte
        if len(conf.RemoteKubeconfig) != 0 && conf.RemoteKubeconfig[0] == '~' {
            conf.RemoteKubeconfig = filepath.Join("/home", conf.User, conf.RemoteKubeconfig[1:])
        }
        if conf.RemoteKubeconfig == "" {
            // if `--remote-kubeconfig` is parsed then Entrypoint is reset
            conf.RemoteKubeconfig = filepath.Join("/home", conf.User, clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName)
        }
        stdout, stderr, err = RemoteRun(cli,
            fmt.Sprintf("sh -c 'kubectl config view --flatten --raw --kubeconfig %s || minikube kubectl -- config view --flatten --raw --kubeconfig %s'",
                conf.RemoteKubeconfig,
                conf.RemoteKubeconfig),
            map[string]string{clientcmd.RecommendedConfigPathEnvVar: conf.RemoteKubeconfig},
        )
        if err != nil {
            err = errors.Wrap(err, string(stderr))
            return
        }
        if len(stdout) == 0 {
            err = errors.Errorf("can not get kubeconfig %s from remote ssh server: %s", conf.RemoteKubeconfig, string(stderr))
            return
        }

        var temp *os.File
        if temp, err = os.CreateTemp("", "*.kubeconfig"); err != nil {
            return
        }
        if err = temp.Close(); err != nil {
            return
        }
        if err = os.WriteFile(temp.Name(), stdout, 0644); err != nil {
            return
        }
        if err = os.Chmod(temp.Name(), 0644); err != nil {
            return
        }
        configFlags.KubeConfig = pointer.String(temp.Name())
    } else {
        if flags != nil {
            lookup := flags.Lookup("kubeconfig")
            if lookup != nil {
                if lookup.Value != nil && lookup.Value.String() != "" {
                    configFlags.KubeConfig = pointer.String(lookup.Value.String())
                } else if lookup.DefValue != "" {
                    configFlags.KubeConfig = pointer.String(lookup.DefValue)
                }
            }
        }
    }
    matchVersionFlags := util.NewMatchVersionFlags(configFlags)
    var rawConfig api.Config
    rawConfig, err = matchVersionFlags.ToRawKubeConfigLoader().RawConfig()
    if err != nil {
        return
    }
    if err = api.FlattenConfig(&rawConfig); err != nil {
        return
    }
    if rawConfig.Contexts == nil {
        err = errors.New("kubeconfig is invalid")
        return
    }
    kubeContext := rawConfig.Contexts[rawConfig.CurrentContext]
    if kubeContext == nil {
        err = errors.New("kubeconfig is invalid")
        return
    }
    cluster := rawConfig.Clusters[kubeContext.Cluster]
    if cluster == nil {
        err = errors.New("kubeconfig is invalid")
        return
    }
    var u *url.URL
    u, err = url.Parse(cluster.Server)
    if err != nil {
        return
    }

    serverHost := u.Hostname()
    serverPort := u.Port()
    if serverPort == "" {
        if u.Scheme == "https" {
            serverPort = "443"
        } else if u.Scheme == "http" {
            serverPort = "80"
        } else {
            // handle other schemes if necessary
            err = errors.New("kubeconfig is invalid: wrong protocol")
            return
        }
    }
    ips, err := net.LookupHost(serverHost)
    if err != nil {
        return
    }

    if len(ips) == 0 {
        // handle error: no IP associated with the hostname
        err = fmt.Errorf("kubeconfig: no IP associated with the hostname %s", serverHost)
        return
    }

    var remote netip.AddrPort
    // Use the first IP address
    remote, err = netip.ParseAddrPort(net.JoinHostPort(ips[0], serverPort))
    if err != nil {
        return
    }
    var port int
    port, err = pkgutil.GetAvailableTCPPortOrDie()
    if err != nil {
        return
    }
    var local netip.AddrPort
    local, err = netip.ParseAddrPort(net.JoinHostPort("127.0.0.1", strconv.Itoa(port)))
    if err != nil {
        return
    }

    if print {
        log.Infof("Waiting jump to bastion host...")
        log.Debugf("Root daemon jumping to ssh host for kubeconfig %s ...", ptr.Deref(configFlags.KubeConfig, ""))
    } else {
        log.Debugf("User daemon jumping to ssh host for kubeconfig %s ...", ptr.Deref(configFlags.KubeConfig, ""))
    }
    err = PortMapUntil(ctx, conf, remote, local)
    if err != nil {
        log.Errorf("SSH port map error: %v", err)
        return
    }

    rawConfig.Clusters[rawConfig.Contexts[rawConfig.CurrentContext].Cluster].Server = fmt.Sprintf("%s://%s", u.Scheme, local.String())
    rawConfig.Clusters[rawConfig.Contexts[rawConfig.CurrentContext].Cluster].TLSServerName = serverHost
    // To Do: add cli option to skip tls verify
    // rawConfig.Clusters[rawConfig.Contexts[rawConfig.CurrentContext].Cluster].CertificateAuthorityData = nil
    // rawConfig.Clusters[rawConfig.Contexts[rawConfig.CurrentContext].Cluster].InsecureSkipTLSVerify = true
    rawConfig.SetGroupVersionKind(schema.GroupVersionKind{Version: latest.Version, Kind: "Config"})

    var convertedObj runtime.Object
    convertedObj, err = latest.Scheme.ConvertToVersion(&rawConfig, latest.ExternalVersion)
    if err != nil {
        return
    }
    var marshal []byte
    marshal, err = json.Marshal(convertedObj)
    if err != nil {
        return
    }
    var temp *os.File
    temp, err = os.CreateTemp("", "*.kubeconfig")
    if err != nil {
        return
    }
    if err = temp.Close(); err != nil {
        return
    }
    if err = os.WriteFile(temp.Name(), marshal, 0644); err != nil {
        return
    }
    if err = os.Chmod(temp.Name(), 0644); err != nil {
        return
    }
    if print {
        msg := fmt.Sprintf("To use: export KUBECONFIG=%s", temp.Name())
        PrintLine(log.Info, msg)
        log.Debugf("Root daemon jump ssh bastion host with kubeconfig: %s", temp.Name())
    } else {
        log.Debugf("User daemon jump ssh bastion host with kubeconfig: %s", temp.Name())
    }
    path = temp.Name()
    return
}

func PrintLine(f func(...any), msg ...string) {
    var length = -1
    for _, s := range msg {
        length = max(len(s), length)
    }
    if f == nil {
        f = func(a ...any) {
            fmt.Println(a...)
        }
    }
    line := "+" + strings.Repeat("-", length+2) + "+"
    f(line)
    for _, s := range msg {
        var padding string
        if length != len(s) {
            padding = strings.Repeat(" ", length-len(s))
        }
        f(fmt.Sprintf("| %s%s |", s, padding))
    }
    f(line)
}

func SshJumpAndSetEnv(ctx context.Context, conf *SshConfig, flags *pflag.FlagSet, print bool) error {
    if conf.Addr == "" && conf.ConfigAlias == "" {
        return nil
    }
    path, err := SshJump(ctx, conf, flags, print)
    if err != nil {
        return err
    }
    err = os.Setenv(clientcmd.RecommendedConfigPathEnvVar, path)
    if err != nil {
        return err
    }
    return os.Setenv(config.EnvSSHJump, path)
}