Skip to content

Latest commit

 

History

History
563 lines (443 loc) · 17.5 KB

File metadata and controls

563 lines (443 loc) · 17.5 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 {
        return err
}
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.Call function.

package main

import (
        "context"
        "fmt"
        "os"
        "time"

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

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

        ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
        defer cancel()

        m, err := s.Call(ctx, "version", nil)
        if err != nil {
                return err
        }

        fmt.Println(m)

        return nil
}

func main() {
        if err := showVersion(); err != nil {
		        fmt.Fprintln(os.Stderr, "Error:", err)
                os.Exit(1)
        }
}

On my machine, this gives me:

{
  daemon = charon-systemd
  version = 5.9.13
  sysname = Linux
  release = 6.14.0-29-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-conns command, work by streaming a certain event type for the duration of the command request. In the case of list-conns, charon streams messages of the list-conn event type. We can make these types of commmand requests with Session.CallStreaming. 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-conns example. We know the name of the command, list-conns, and we know we need to tell the daemon to stream list-conn events. If we look at the command's message parameters, we see one optional parameter:

{
	ike = <list connections matching a given configuration name only>
} => {
	# completes after streaming list-conn events
}

Where each list-conn event contains the following information:

{
	<IKE_SA connection name> = {
		local_addrs = [
			<list of valid local IKE endpoint addresses>
		]
		remote_addrs = [
			<list of valid remote IKE endpoint addresses>
		]
		local_port = <local IKE endpoint port>
		remote_port = <remote IKE endpoint port>
		version = <IKE version as string, IKEv1|IKEv2 or 0 for any>
		reauth_time = <IKE_SA reauthentication interval in seconds>
		rekey_time = <IKE_SA rekeying interval in seconds>

		local*, remote* = { # multiple local and remote auth sections
			class = <authentication type>
			eap-type = <EAP type to authenticate if when using EAP>
			eap-vendor = <EAP vendor for type, if any>
			xauth = <xauth backend name>
			revocation = <revocation policy>
			id = <IKE identity>
			aaa_id = <AAA authentication backend identity>
			eap_id = <EAP identity for authentication>
			xauth_id = <XAuth username for authentication>
			groups = [
				<group membership required to use connection>
			]
			certs = [
				<certificates allowed for authentication>
			]
			cacerts = [
				<CA certificates allowed for authentication>
			]
		}
		children = {
			<CHILD_SA config name>* = {
				mode = <IPsec mode>
				label = <hex encoded security label>
				rekey_time = <CHILD_SA rekeying interval in seconds>
				rekey_bytes = <CHILD_SA rekeying interval in bytes>
				rekey_packets = <CHILD_SA rekeying interval in packets>
				local-ts = [
					<list of local traffic selectors>
				]
				remote-ts = [
					<list of remote traffic selectors>
				]
			}
		}
	}
}

So, let's write a simple program to print the loaded connections, optionally by IKE name:

package main

import (
        "context"
        "flag"
        "fmt"
        "os"
        "time"

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

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

        var in *vici.Message

        if ike != "" {
                in = vici.NewMessage()

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

        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()

        for m, err := range s.CallStreaming(ctx, "list-conns", "list-conn", in) {
                if err != nil {
                        return err
                }

                fmt.Println(m)
        }

        return nil
}

func main() {
        var ike string

        flag.StringVar(&ike, "ike", "", "Filter conns by IKE SA name")
        flag.Parse()

        if err := showConns(ike); err != nil {
		        fmt.Fprintln(os.Stderr, "Error:", err)
                os.Exit(1)
        }
}

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 (
        "errors"
        "flag"
        "fmt"
        "os"

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

func monitor(ike string) error {
        if ike == "" {
                return errors.New("must specify IKE SA name")
        }

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

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

        s.NotifyEvents(ec)
        defer s.StopEvents(ec)

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

        for {
                e, ok := <-ec
                if !ok {
                        // Event listener closed.
                        return nil
                }

                // 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(ike).(*vici.Message)
                        if !ok {
                                // This message is not about the IKE SA we are
                                // monitoring. Ignore.
                                continue
                        }

                        fmt.Printf("IKE-UPDOWN: %s state changed: %s\n", ike, m.Get("state"))
                case "log":
                        if s, ok := e.Message.Get("ikesa-name").(string); !ok || s != ike {
                                // This message is not about the IKE SA we are
                                // monitoring. Ignore.
                                continue
                        }

                        // Log events contain a 'msg' field with the log message
                        fmt.Println("LOG:", e.Message.Get("msg"))
                }
        }
}

func main() {
        var ike string

        flag.StringVar(&ike, "ike", "", "Name of IKE SA to monitor.")
        flag.Parse()

        if err := monitor(ike); err != nil {
                fmt.Fprintln(os.Stderr, "Error:", err)
                os.Exit(1)
        }
}

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 (
        "context"
        "encoding/pem"
        "errors"
        "flag"
        "fmt"
        "io/ioutil"
        "os"
        "time"

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

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

func loadX509Cert(path string) error {
        if path == "" {
                return errors.New("must specify path")
        }

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

        // 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: "NONE",
                Data: string(block.Bytes),
        }

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

        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
        defer cancel()

        _, err = s.Call(ctx, "load-cert", in)

        return err
}

func main() {
        var path string

        flag.StringVar(&path, "path", "", "Path to certificate to load")
        flag.Parse()

        if err := loadX509Cert(path); err != nil {
                fmt.Fprintln(os.Stderr, "Error:", err)
                os.Exit(1)
        }
}

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 (
        "context"
        "fmt"
        "time"

        "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(s *vici.Session, conn connection) error {
	c, err := vici.MarshalMessage(&conn)
	if err != nil {
            return err
	}

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

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	_, err = s.Call(ctx, "load-conn", in)

	return err
}

func initiate(s *vici.Session, ike, child string) error {
        in := vici.NewMessage()

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

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

        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()

        for m, err := range s.CallStreaming(ctx, "initiate", "control-log", in) {
                if err != nil {
                        return err
                }

                fmt.Println("LOG:", m.Get("msg"))
        }

        return nil
}