Skip to content

Commit

Permalink
Merge pull request #209 from zaneb/uri-dialers
Browse files Browse the repository at this point in the history
Add TLS and SSH Dialers, and support libvirt URI parsing
  • Loading branch information
trapgate authored Feb 20, 2024
2 parents fcb3518 + ef7bc72 commit fcabe97
Show file tree
Hide file tree
Showing 8 changed files with 798 additions and 56 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ Peter Kurfer <[email protected]>
Sam Roberts <[email protected]>
Moritz Wanzenböck <[email protected]>
Jenni Griesmann <[email protected]>
Zane Bitter <[email protected]>
57 changes: 11 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,22 +130,16 @@ import (
"fmt"
"log"
"net"
"net/url"
"time"

"github.com/digitalocean/go-libvirt"
)

func main() {
// This dials libvirt on the local machine, but you can substitute the first
// two parameters with "tcp", "<ip address>:<port>" to connect to libvirt on
// a remote machine.
c, err := net.DialTimeout("unix", "/var/run/libvirt/libvirt-sock", 2*time.Second)
uri, _ := url.Parse(string(libvirt.QEMUSystem))
l, err := libvirt.ConnectToURI(uri)
if err != nil {
log.Fatalf("failed to dial libvirt: %v", err)
}

l := libvirt.New(c)
if err := l.Connect(); err != nil {
log.Fatalf("failed to connect: %v", err)
}

Expand Down Expand Up @@ -196,53 +190,24 @@ import (
"log"

"github.com/digitalocean/go-libvirt"
"github.com/digitalocean/go-libvirt/socket/dialers"
)

func main() {
// This dials libvirt on the local machine
// It connects to libvirt via TLS over TCP
// To connect to a remote machine, you need to have the ca/cert/key of it.
keyFileXML, err := ioutil.ReadFile("/etc/pki/libvirt/private/clientkey.pem")
if err != nil {
log.Fatalf("%v", err)
}

certFileXML, err := ioutil.ReadFile("/etc/pki/libvirt/clientcert.pem")
if err != nil {
log.Fatalf("%v", err)
}

caFileXML, err := ioutil.ReadFile("/etc/pki/CA/cacert.pem")
if err != nil {
log.Fatalf("%v", err)
}
cert, err := tls.X509KeyPair([]byte(certFileXML), []byte(keyFileXML))
if err != nil {
log.Fatalf("%v", err)
}

roots := x509.NewCertPool()
roots.AppendCertsFromPEM([]byte(caFileXML))

config := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: roots,
}
// The private key is at ~/.pki/libvirt/clientkey.pem
// or /etc/pki/libvirt/private/clientkey.pem
// The Client Cert is at ~/.pki/libvirt/clientcert.pem
// or /etc/pki/libvirt/clientcert.pem
// The CA Cert is at ~/.pki/libvirt/cacert.pem
// or /etc/pki/CA/cacert.pem

// Use host name or IP which is valid in certificate
addr := "10.10.10.10"
port := "16514"
c, err := tls.Dial("tcp", addr + ":" + port, config)
if err != nil {
log.Fatalf("failed to dial libvirt: %v", err)
}

// Drop a byte before libvirt.New(c)
// More details at https://github.com/digitalocean/go-libvirt/issues/89
// Remove this line if the issue does not exist any more
c.Read(make([]byte, 1))

l := libvirt.New(c)
l := libvirt.NewWithDialer(dialers.NewTLS(addr))
if err := l.Connect(); err != nil {
log.Fatalf("failed to connect: %v", err)
}
Expand Down
217 changes: 217 additions & 0 deletions connect_uri.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package libvirt

import (
"errors"
"fmt"
"net/url"
"os/user"
"strconv"
"strings"

"github.com/digitalocean/go-libvirt/socket"
"github.com/digitalocean/go-libvirt/socket/dialers"
)

// ConnectToURI returns a new, connected client instance using the appropriate
// dialer for the given libvirt URI.
func ConnectToURI(uri *url.URL) (*Libvirt, error) {
dialer, err := dialerForURI(uri)
if err != nil {
return nil, err
}

lv := NewWithDialer(dialer)

if err := lv.ConnectToURI(RemoteURI(uri)); err != nil {
return nil, fmt.Errorf("failed to connect to libvirt: %w", err)
}

return lv, nil
}

// RemoteURI returns the libvirtd URI corresponding to a given client URI.
// The client URI contains details of the connection method, but once connected
// to libvirtd, all connections are local. So e.g. the client may want to
// connect to qemu+tcp://example.com/system but once the socket is established
// it will ask the remote libvirtd for qemu:///system.
func RemoteURI(uri *url.URL) ConnectURI {
remoteURI := (&url.URL{
Scheme: strings.Split(uri.Scheme, "+")[0],
Path: uri.Path,
}).String()
if name := uri.Query().Get("name"); name != "" {
remoteURI = name
}
return ConnectURI(remoteURI)
}

func dialerForURI(uri *url.URL) (socket.Dialer, error) {
transport := "unix"
if scheme := strings.SplitN(uri.Scheme, "+", 2); len(scheme) > 1 {
transport = scheme[1]
} else if uri.Host != "" {
transport = "tls"
}

switch transport {
case "unix":
options := []dialers.LocalOption{}
if s := uri.Query().Get("socket"); s != "" {
options = append(options, dialers.WithSocket(s))
}
if err := checkModeOption(uri); err != nil {
return nil, err
}
return dialers.NewLocal(options...), nil
case "tcp":
options := []dialers.RemoteOption{}
if port := uri.Port(); port != "" {
options = append(options, dialers.UsePort(port))
}
return dialers.NewRemote(uri.Hostname(), options...), nil
case "tls":
options := []dialers.TLSOption{}
if port := uri.Port(); port != "" {
options = append(options, dialers.UseTLSPort(port))
}
if pkiPath := uri.Query().Get("pkipath"); pkiPath != "" {
options = append(options, dialers.UsePKIPath(pkiPath))
}
if nv, err := noVerifyOption(uri); err != nil {
return nil, err
} else if nv {
options = append(options, dialers.WithInsecureNoVerify())
}
return dialers.NewTLS(uri.Hostname(), options...), nil
case "libssh", "libssh2":
options := []dialers.SSHOption{}
options, err := processCommonSSHOptions(uri, options)
if err != nil {
return nil, err
}
if knownHosts := uri.Query().Get("known_hosts"); knownHosts != "" {
options = append(options, dialers.UseKnownHostsFile(knownHosts))
}
if hostVerify := uri.Query().Get("known_hosts_verify"); hostVerify != "" {
switch hostVerify {
case "normal":
case "auto":
options = append(options, dialers.WithAcceptUnknownHostKey())
case "ignore":
options = append(options, dialers.WithInsecureIgnoreHostKey())
default:
return nil, fmt.Errorf("invalid ssh known hosts verify method %v", hostVerify)
}
}
if auth := uri.Query().Get("sshauth"); auth != "" {
authMethods := &dialers.SSHAuthMethods{}
for _, a := range strings.Split(auth, ",") {
switch strings.ToLower(a) {
case "agent":
authMethods.Agent()
case "privkey":
authMethods.PrivKey()
case "password":
authMethods.Password()
case "keyboard-interactive":
authMethods.KeyboardInteractive()
default:
return nil, fmt.Errorf("invalid ssh auth method %v", a)
}
}
options = append(options, dialers.WithSSHAuthMethods(authMethods))
}
if noVerify := uri.Query().Get("no_verify"); noVerify != "" {
return nil, fmt.Errorf(
"\"no_verify\" option invalid with %s transport, use known_hosts_verify=ignore instead",
transport)
}
return dialers.NewSSH(uri.Hostname(), options...), nil
case "ssh":
// Emulate ssh using golang ssh library. Note that this means that
// system ssh config is not respected as it would be when shelling out
// to the ssh binary.
currentUser, err := user.Current()
if err != nil {
return nil, err
}
options := []dialers.SSHOption{
dialers.WithSystemSSHDefaults(currentUser),
}
options, err = processCommonSSHOptions(uri, options)
if err != nil {
return nil, err
}
if nv, err := noVerifyOption(uri); err != nil {
return nil, err
} else if nv {
options = append(options, dialers.WithInsecureIgnoreHostKey())
}

fieldErrs := []error{}
for _, f := range []string{
"known_hosts",
"known_hosts_verify",
"sshauth",
} {
if field := uri.Query().Get(f); field != "" {
fieldErrs = append(fieldErrs,
fmt.Errorf("%v option invalid with ssh transport, use libssh transport instead", f))
}
}
if len(fieldErrs) > 0 {
return nil, errors.Join(fieldErrs...)
}

return dialers.NewSSH(uri.Hostname(), options...), nil
default:
return nil, fmt.Errorf("unsupported libvirt transport %s", transport)
}
}

func noVerifyOption(uri *url.URL) (bool, error) {
nv := uri.Query().Get("no_verify")
if nv == "" {
return false, nil
}
val, err := strconv.Atoi(nv)
if err != nil {
return false, fmt.Errorf("invalid value for no_verify: %w", err)
}
return val != 0, nil
}

func checkModeOption(uri *url.URL) error {
mode := uri.Query().Get("mode")
switch strings.ToLower(mode) {
case "":
case "legacy", "auto":
case "direct":
return errors.New("cannot connect in direct mode")
default:
return fmt.Errorf("invalid ssh mode %v", mode)
}
return nil
}

func processCommonSSHOptions(uri *url.URL, options []dialers.SSHOption) ([]dialers.SSHOption, error) {
if port := uri.Port(); port != "" {
options = append(options, dialers.UseSSHPort(port))
}
if username := uri.User.Username(); username != "" {
options = append(options, dialers.UseSSHUsername(username))
}
if password, ok := uri.User.Password(); ok {
options = append(options, dialers.UseSSHPassword(password))
}
if socket := uri.Query().Get("socket"); socket != "" {
options = append(options, dialers.WithRemoteSocket(socket))
}
if keyFile := uri.Query().Get("keyfile"); keyFile != "" {
options = append(options, dialers.UseKeyFile(keyFile))
}
if err := checkModeOption(uri); err != nil {
return options, err
}
return options, nil
}
12 changes: 3 additions & 9 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,16 @@
// "fmt"
// "log"
// "net"
// "net/url"
// "time"
//
// "github.com/digitalocean/go-libvirt"
// )
//
// func main() {
// // This dials libvirt on the local machine, but you can substitute the first
// // two parameters with "tcp", "<ip address>:<port>" to connect to libvirt on
// // a remote machine.
// c, err := net.DialTimeout("unix", "/var/run/libvirt/libvirt-sock", 2*time.Second)
// uri, _ := url.Parse(string(libvirt.QEMUSystem))
// l, err := libvirt.ConnectToURI(uri)
// if err != nil {
// log.Fatalf("failed to dial libvirt: %v", err)
// }
//
// l := libvirt.New(c)
// if err := l.Connect(); err != nil {
// log.Fatalf("failed to connect: %v", err)
// }
//
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ go 1.15

require (
github.com/stretchr/testify v1.8.1
golang.org/x/tools v0.1.12
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/tools v0.6.0
)
17 changes: 17 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,45 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
Loading

0 comments on commit fcabe97

Please sign in to comment.