Skip to content

Latest commit

 

History

History
459 lines (360 loc) · 14.1 KB

getting_started.md

File metadata and controls

459 lines (360 loc) · 14.1 KB

Getting Started with vici

Command Requests

Let's start with a simple example to try and understand how vici works. If you are running strongswan with the charon daemon, and the vici plugin is enabled (the default), you can create a vici client session like this:

s, err := vici.NewSession()
if err != nil {
        fmt.Println(err)
        return
}
defer s.Close() 

Say we wanted to get the version information of the charon daemon running on our system. If we look at the vici README, we can find the version command in the Client-initiated commands section. The README gives the following definition of the version command's message parameters:

{} => {
    daemon = <IKE daemon name>
    version = <strongSwan version>
    sysname = <operating system name>
    release = <operating system release>
    machine = <hardware identifier>
}

This means that the command does not accept any arguments, and returns five key-value pairs. So, there is no need to construct a request message for this command. Now all we have to do is make a command request using the Session.CommandRequest function.

package main

import (
        "fmt"

        "github.com/strongswan/govici/vici"
)

func main() {
        session, err := vici.NewSession()
        if err != nil {
                fmt.Println(err)
                return
        }
        defer session.Close()

        m, err := session.CommandRequest("version", nil)
        if err != nil {
                fmt.Println(err)
                return
        }

        for _, k := range m.Keys() {
                fmt.Printf("%v: %v\n", k, m.Get(k))
        }
}

On my machine, this gives me:

daemon: charon
version: 5.6.2
sysname: Linux
release: 4.15.0-72-generic
machine: x86_64

Streamed Command Requests

Another important concept in vici is server-issued events. A complete list of defined events can be found in the Server-issued events section of the vici README. Some commands, for example the list-certs command, work by streaming a certain event type for the duration of the command request. In the case of list-certs, charon streams messages of the list-cert event type. We can make these types of commmand requests with Session.StreamedCommandRequest. When making a streamed command request, it is our job to tell the daemon which type of event we want to listen for during the command.

Let's continue with the list-certs example. We know the name of the command, list-certs, and we know we need to tell the daemon to stream list-cert events. If we look at the command's message parameters, we see some optional parameters:

{
    type = <certificate type to filter for, X509|X509_AC|X509_CRL|
                                            OCSP_RESPONSE|PUBKEY  or ANY>
    flag = <X.509 certificate flag to filter for, NONE|CA|AA|OCSP or ANY>
    subject = <set to list only certificates having subject>
} => {
    # completes after streaming list-cert events
}

Where each list-cert event contains the following information:

{
    type = <certificate type, X509|X509_AC|X509_CRL|OCSP_RESPONSE|PUBKEY>
    flag = <X.509 certificate flag, NONE|CA|AA|OCSP>
    has_privkey = <set if a private key for the certificate is available>
    data = <ASN1 encoded certificate data>
    subject = <subject string if defined and certificate type is PUBKEY>
    not-before = <time string if defined and certificate type is PUBKEY>
    not-after  = <time string if defined and certificate type is PUBKEY>
}

So, let's say we wanted to filter the results to only list CA certs. We can accomplish this by doing the following:

package main

import (
        "fmt"

        "github.com/strongswan/govici/vici"
)

func main() {
        session, err := vici.NewSession()
        if err != nil {
                fmt.Println(err)
                return
        }
        defer session.Close()

        m := vici.NewMessage()
        
        if err := m.Set("flag", "CA"); err != nil {
                fmt.Println(err)
                return
        }

        ms, err := session.StreamedCommandRequest("list-certs", "list-cert", m)
        if err != nil {
                fmt.Println(err)
                return
        }

        for _, m := range ms {
                if m.Err() != nil {
                        fmt.Println(err)
                        return
                }
                
                // Process CA cert information
                // ...        
        }
}

Event Listener

A Session can also be used to listen for specific server-issued events at any time, not only during streamed command requests. This is done with the Session.Subscribe function, which accepts a list of event types. As an example, say we wanted to create a routine to monitor the state of a given SA, as well as log events. We can register the Session's event listener to listen for the ike-updown and log events like this:

package main

import (
	"fmt"

	"github.com/strongswan/govici/vici"
)

func main() {
	session, err := vici.NewSession()
	if err != nil {
		fmt.Println(err)
		return
	}
	defer session.Close()

	ec := make(chan vici.Event, 16)

	session.NotifyEvents(ec)
	defer session.StopEvents(ec)

	// Subscribe to 'ike-updown' and 'log' events.
	if err := session.Subscribe("ike-updown", "log"); err != nil {
		fmt.Println(err)
		return
	}

	// IKE SA configuration name
	name := "rw"

	for {
		e, ok := <-ec
		if !ok {
			fmt.Println("Event listener closed")
			return
		}

		// The Event.Name field corresponds to the event name
		// we used to make the subscription. The Event.Message
		// field contains the Message from the server.
		switch e.Name {
		case "ike-updown":
			m, ok := e.Message.Get(name).(*vici.Message)
			if !ok {
				fmt.Printf("Expected *Message in field 'name', but got %T\n", e.Message.Get(name))
				continue
			}

			state := m.Get("state")
			fmt.Printf("IKE SA state changed (name=%s): %s\n", name, state)
		case "log":
			// Log events contain a 'msg' field with the log message
			fmt.Println(e.Message.Get("msg"))
		}
	}
}

The Session.NotifyEvents function is used to register a channel to receive Event's on. The channel will continue to receive events as long as the Session is subscribed to events, or until Session.StopEvents is called with the same channel. Event subscriptions and unsubscriptions can be made at any time while the Session is active.

Message Marshaling

Some commands require a lot of parameters, or even a whole IKE SA configuration in the case of load-conn. Using Message.Set for this sort of thing is not very flexible and is quite cumbersome. The MarshalMessage function provides a way to easily construct a Message from a Go struct. To start with a simple example, let's define a struct cert that can be used to load certificates into the daemon using the load-cert command. If we look at the vici README again, we see the load-cert command's message parameters:

{
    type = <certificate type, X509|X509_AC|X509_CRL>
    flag = <X.509 certificate flag, NONE|CA|AA|OCSP>
    data = <PEM or DER encoded certificate data>
} => {
    success = <yes or no>
    errmsg = <error string on failure>
}

So our Go struct is simple:

type cert struct {
        Type string `vici:"type"`
        Flag string `vici:"flag"`
        Data string `vici:"data"`
}

Remember, as stated on godoc, struct fields are only marshaled when they are exported and have a vici struct tag. Notice that the struct tags are identical to the field names in the load-cert message parameters. Now, we could wrap this all up into a helper function that loads a certificate into the daemon given its path on the filesystem.

package main

import (
	"encoding/pem"
	"io/ioutil"

	"github.com/strongswan/govici/vici"
)

type cert struct {
	Type string `vici:"type"`
	Flag string `vici:"flag"`
	Data string `vici:"data"`
}

func loadX509Cert(path string, cacert bool) error {
	s, err := vici.NewSession()
	if err != nil {
		return err
	}
	defer s.Close()

	flag := "NONE"
	if cacert {
		flag = "CA"
	}

	// Read cert data from the file
	data, err := ioutil.ReadFile(path)
	if err != nil {
		return err
	}

	block, _ := pem.Decode(data)

	cert := cert{
		Type: "X509",
		Flag: flag,
		Data: string(block.Bytes),
	}

	m, err := vici.MarshalMessage(&cert)
	if err != nil {
		return err
	}

	_, err = s.CommandRequest("load-cert", m)

	return err
}

Pointer types can be useful to preserve defaults as specified in swanctl.conf when those defaults do not align with Go zero values. For example, mobike is enabled by default for IKEv2 connections, but if you have a Mobike bool field in your struct, the Go zero value will override the default behavior. In these situations, using *bool will result in the zero-value being nil, and the field will not be marshaled.

Putting it all together

For a more complicated example, let's use load-conn to load an IKE SA configuration. The real work in doing this is defining some types to represent our configuration. The swanctl.conf documentation is the best place to look for the information we need about configuration options and structure. For our case, let's take a swanctl.conf from the testing environment:

connections {

   rw {
      local_addrs  = 192.168.0.1

      local {
         auth = pubkey
         certs = moonCert.pem
         id = moon.strongswan.org
      }
      remote {
         auth = pubkey
      }
      children {
         net {
            local_ts  = 10.1.0.0/16 

            updown = /usr/local/libexec/ipsec/_updown iptables
            esp_proposals = aes128gcm128-x25519
         }
      }
      version = 2
      proposals = aes128-sha256-x25519
   }
}

We'll create Go types that satisfy the needs of this specific configuration, a common "road warrior" scenario with certificate-based authentication. See here for details on this testing scenario. If you're more familiar with the ipsec.conf configuration format, see this document for help migrating to the swanctl.conf format.

We can start by defining a type for a connection, where the fields correspond to the connections.<conn>.* fields defined in the swanctl.conf documentation.

type connection struct {
	Name string // This field will NOT be marshaled!

	LocalAddrs []string            `vici:"local_addrs"`
	Local      *localOpts          `vici:"local"`
	Remote     *remoteOpts         `vici:"remote"`
	Children   map[string]*childSA `vici:"children"`
	Version    int                 `vici:"version"`
	Proposals  []string            `vici:"proposals"`
}

Then, we need to define localOpts and remoteOpts as referenced in the above definition:

type localOpts struct {
	Auth  string   `vici:"auth"`
	Certs []string `vici:"certs"`
	ID    string   `vici:"id"`
}

type remoteOpts struct {
	Auth string `vici:"auth"`
}

Remember, in this example, we only include the fields that are needed for our particular swanctl.conf. But any options from the connections.<conn>.local<suffix> or connections.<conn>.remote<suffix> sections could be defined here.

Finally, we need a childSA type:

type childSA struct {
	LocalTrafficSelectors []string `vici:"local_ts"`
	Updown                string   `vici:"updown"`
	ESPProposals          []string `vici:"esp_proposals"`
}

Putting this all together, we can write some helpers to load our configuration into the daemon, and then establish the SAs.

package main

import (
        "github.com/strongswan/govici/vici"
)

type connection struct {
	Name string // This field will NOT be marshaled!

	LocalAddrs []string            `vici:"local_addrs"`
	Local      *localOpts          `vici:"local"`
	Remote     *remoteOpts         `vici:"remote"`
	Children   map[string]*childSA `vici:"children"`
	Version    int                 `vici:"version"`
	Proposals  []string            `vici:"proposals"`
}

type localOpts struct {
	Auth  string   `vici:"auth"`
	Certs []string `vici:"certs"`
	ID    string   `vici:"id"`
}

type remoteOpts struct {
	Auth string `vici:"auth"`
}

type childSA struct {
	LocalTrafficSelectors []string `vici:"local_ts"`
	Updown                string   `vici:"updown"`
	ESPProposals          []string `vici:"esp_proposals"`
}

func loadConn(conn connection) error {
	s, err := vici.NewSession()
	if err != nil {
		return err
	}
        defer s.Close()

	c, err := vici.MarshalMessage(&conn)
	if err != nil {
		return err
	}

        m := vici.NewMessage()
        if err := m.Set(conn.Name, c); err != nil {
                return err
        }

	_, err = s.CommandRequest("load-conn", m)

	return err
}

func initiate(ike, child string) error {
	s, err := vici.NewSession()
	if err != nil {
		return err
	}
        defer s.Close()

	m := vici.NewMessage()

	if err := m.Set("child", child); err != nil {
		return err
	}

	if err := m.Set("ike", ike); err != nil {
		return err
	}

	ms, err := s.StreamedCommandRequest("initiate", "control-log", m)
	if err != nil {
		return err
	}

	for _, msg := range ms {
		if err := msg.Err(); err != nil {
                        return err
		}
	}

	return nil
}