diff --git a/client/go.mod b/client/go.mod index a80c760..f6b0d0d 100644 --- a/client/go.mod +++ b/client/go.mod @@ -3,7 +3,7 @@ module main go 1.18 require ( - github.com/opiproject/opi-api v0.0.0-20220809145755-77ea7f55985a + github.com/opiproject/opi-api v0.0.0-20220811205735-2cdd70e3c937 google.golang.org/grpc v1.48.0 ) diff --git a/client/go.sum b/client/go.sum index a00ccc9..aad6a69 100644 --- a/client/go.sum +++ b/client/go.sum @@ -45,8 +45,8 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/opiproject/opi-api v0.0.0-20220809145755-77ea7f55985a h1:3cLfwe8RPdFtKrOy3PAOx1+erxL/EYU/VyfxfuU5g5I= -github.com/opiproject/opi-api v0.0.0-20220809145755-77ea7f55985a/go.mod h1:92pv4ulvvPMuxCJ9ND3aYbmBfEMLx0VCjpkiR7ZTqPY= +github.com/opiproject/opi-api v0.0.0-20220811205735-2cdd70e3c937 h1:+DlDnApZMdNv8Bpx+Dmrj0XBCaXIewFVIahEWdIyb5U= +github.com/opiproject/opi-api v0.0.0-20220811205735-2cdd70e3c937/go.mod h1:92pv4ulvvPMuxCJ9ND3aYbmBfEMLx0VCjpkiR7ZTqPY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= diff --git a/client/ipsec.go b/client/ipsec.go index 05e92c0..f150b91 100644 --- a/client/ipsec.go +++ b/client/ipsec.go @@ -18,6 +18,7 @@ func do_ipsec(conn grpc.ClientConnInterface, ctx context.Context) { // Create IPsec Connection local_ipsec := pb.IPsecCreateRequest{ + Name: "opi-test", Tunnel: &pb.TunnelInterfaces{ Tunnels: []*pb.TunnelInterfaces_Tunnel{ { @@ -27,6 +28,7 @@ func do_ipsec(conn grpc.ClientConnInterface, ctx context.Context) { LocalSpi: rand.Uint32(), CryptoAlg: pb.CryptoAlgorithm_AES256GCM128, IntegAlg: pb.IntegAlgorithm_SHA256_96, + Dhgroups: pb.DiffieHellmanGroups_CURVE25519, Mode: pb.IPsecMode_TUNNEL_MODE, }, }, @@ -39,6 +41,7 @@ func do_ipsec(conn grpc.ClientConnInterface, ctx context.Context) { Protocol: pb.SecurityAssociations_Sa_ESP, CryptoAlg: pb.CryptoAlgorithm_AES256GMAC, IntegAlg: pb.IntegAlgorithm_SHA512, + Dhgroups: pb.DiffieHellmanGroups_CURVE25519, }, }, }, diff --git a/conf/client/eap-tls.conf b/conf/client/eap-tls.conf deleted file mode 100644 index 6de8808..0000000 --- a/conf/client/eap-tls.conf +++ /dev/null @@ -1,24 +0,0 @@ -eap-tls { - remote_addrs = 192.168.0.2 - vips = 0.0.0.0 - - local { - auth = eap-tls - eap_id = client.strongswan.org - } - remote { - auth = pubkey - id = server.strongswan.org - } - children { - eap-tls { - remote_ts = 10.1.0.0/16,192.168.0.2 - - esp_proposals = aes256gcm128-x25519 - dpd_action = trap - } - } - version = 2 - proposals = aes256-sha256-x25519 - dpd_delay = 60s -} diff --git a/conf/client/eap.conf b/conf/client/eap.conf deleted file mode 100644 index 6d7fe94..0000000 --- a/conf/client/eap.conf +++ /dev/null @@ -1,24 +0,0 @@ -eap { - remote_addrs = 192.168.0.2 - vips = 0.0.0.0 - - local { - auth = eap-md5 - eap_id = hacker - } - remote { - auth = pubkey - id = server.strongswan.org - } - children { - eap { - remote_ts = 10.1.0.0/16,192.168.0.2 - - esp_proposals = aes256gcm128-x25519 - dpd_action = trap - } - } - version = 2 - proposals = aes256-sha256-x25519 - dpd_delay = 60s -} diff --git a/conf/client/home.conf b/conf/client/home.conf deleted file mode 100644 index e3b237e..0000000 --- a/conf/client/home.conf +++ /dev/null @@ -1,29 +0,0 @@ -home { - remote_addrs = 192.168.0.2 - vips = 0.0.0.0 - - local { - auth = pubkey - certs = clientCert.pem - id = client.strongswan.org - } - remote { - auth = pubkey - id = server.strongswan.org - } - children { - net { - remote_ts = 10.1.0.0/16 - - esp_proposals = aes256gcm128-chacha20poly1305-x25519 - dpd_action = trap - } - host { - esp_proposals = aes256gcm128-chacha20poly1305-x25519 - dpd_action = trap - } - } - version = 2 - proposals = aes256-sha256-x25519 - dpd_delay = 60s -} diff --git a/conf/client/psk.conf b/conf/client/psk.conf deleted file mode 100644 index 9b3a929..0000000 --- a/conf/client/psk.conf +++ /dev/null @@ -1,25 +0,0 @@ -psk { - remote_addrs = 192.168.0.2 - vips = 0.0.0.0 - - local { - auth = psk - id = hacker@strongswan.org - } - remote { - auth = psk - id = server.strongswan.org - } - children { - psk { - remote_ts = 10.1.0.0/16 - - esp_proposals = chacha20poly1305-x25519 - dpd_action = trap - } - } - version = 2 - proposals = aes256-sha256-x25519 - dpd_delay = 60s - send_certreq = no -} diff --git a/conf/client/swanctl.conf b/conf/client/swanctl.conf index 29e5696..2ca7718 100644 --- a/conf/client/swanctl.conf +++ b/conf/client/swanctl.conf @@ -1,11 +1,3 @@ -connections { - - include home.conf - include psk.conf - include eap.conf - include eap-tls.conf -} - secrets { ike-hacker { diff --git a/conf/strongswan-client.conf b/conf/strongswan-client.conf index 00c1ce0..98f55ff 100644 --- a/conf/strongswan-client.conf +++ b/conf/strongswan-client.conf @@ -8,13 +8,28 @@ charon { } filelog { stderr { - default = 1 + default = 3 + mgr = 1 + ike = 1 + net = 1 + enc = 0 + cfg = 4 + asn = 4 + job = 1 + knl = 1 + lib = 3 } } eap-dynamic { prefer_user = yes preferred = md5, tls } + plugins { + vici { + load = yes + socket = unix:///var/run/charon.vici + } + } } libtls { diff --git a/docker-compose.yml b/docker-compose.yml index 67e8bc1..03002df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,9 +33,11 @@ services: volumes: - ./conf/client:/etc/swanctl - ./conf/strongswan-client.conf:/etc/strongswan.conf + - /var/run networks: internet: ipv4_address: 192.168.0.3 + command: './charon' redis: image: redis:bullseye @@ -52,7 +54,7 @@ services: depends_on: - redis volumes_from: - - vpn-server:rw + - vpn-client:rw networks: - internet - intranet diff --git a/opi-security-poc.drawio.png b/opi-security-poc.drawio.png index b6c6bfb..5f0cc15 100644 Binary files a/opi-security-poc.drawio.png and b/opi-security-poc.drawio.png differ diff --git a/server/go.mod b/server/go.mod index 380148a..b382f2f 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,7 +5,8 @@ go 1.18 require ( github.com/go-redis/redis/v8 v8.11.5 github.com/google/uuid v1.3.0 - github.com/opiproject/opi-api v0.0.0-20220809145755-77ea7f55985a + github.com/opiproject/opi-api v0.0.0-20220811205735-2cdd70e3c937 + github.com/strongswan/govici v0.6.0 google.golang.org/grpc v1.48.0 google.golang.org/protobuf v1.28.1 ) diff --git a/server/go.sum b/server/go.sum index c12c165..56056d3 100644 --- a/server/go.sum +++ b/server/go.sum @@ -57,14 +57,16 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/opiproject/opi-api v0.0.0-20220809145755-77ea7f55985a h1:3cLfwe8RPdFtKrOy3PAOx1+erxL/EYU/VyfxfuU5g5I= -github.com/opiproject/opi-api v0.0.0-20220809145755-77ea7f55985a/go.mod h1:92pv4ulvvPMuxCJ9ND3aYbmBfEMLx0VCjpkiR7ZTqPY= +github.com/opiproject/opi-api v0.0.0-20220811205735-2cdd70e3c937 h1:+DlDnApZMdNv8Bpx+Dmrj0XBCaXIewFVIahEWdIyb5U= +github.com/opiproject/opi-api v0.0.0-20220811205735-2cdd70e3c937/go.mod h1:92pv4ulvvPMuxCJ9ND3aYbmBfEMLx0VCjpkiR7ZTqPY= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/strongswan/govici v0.6.0 h1:QAjc7IIx1c/1P0Hz7yMX91FB5BUNKtyXzfxpuPyBKXE= +github.com/strongswan/govici v0.6.0/go.mod h1:RgO/KrMlFNsRf3dSoxwWSDSV+ASd98n1T+2G3QMEVHE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/server/ipsec.go b/server/ipsec.go index d2eba0c..731dc72 100644 --- a/server/ipsec.go +++ b/server/ipsec.go @@ -54,7 +54,8 @@ func (s *server) IPsecCreate(ctx context.Context, in *pb.IPsecCreateRequest) (*p panic(err) } - err = load_connection("load", in) + //err = load_connection("load", in) + err = loadConn(in) if err != nil { log.Printf("IPsecCreate: Failed with error %v", err) return nil, err @@ -92,12 +93,6 @@ func (s *server) IPsecDelete(ctx context.Context, in *pb.IPsecDeleteRequest) (*p log.Printf("Dumping unmarshaled protobuf\n%v\n", ipsec_req) - err = delete_connection("load", ipsec_req) - if err != nil { - log.Printf("IPsecDelete: Failed with error %v", err) - return nil, err - } - // Delete from Redis rdb.Del(ctx, reqId) diff --git a/server/ipsec_messages.go b/server/ipsec_messages.go deleted file mode 100644 index 00ad207..0000000 --- a/server/ipsec_messages.go +++ /dev/null @@ -1,237 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (c) 2022 Intel Corporation, or its subsidiaries. - -package main - -import ( - "errors" - "fmt" - "log" - "os" - "strings" - "unicode" - -// "github.com/strongswan/govici/vici" - pb "github.com/opiproject/opi-api/security/proto" -) - -//type childSA struct { -// LocalTrafficSelectors []string `vici:"local_ts"` -// Updown string `vici:"updown"` -// ESPProposals []string `vici:"esp_proposals"` -//} - -//type localOpts struct { -// Auth string `vici:"auth"` -// Certs []string `vici:"certs"` -// ID string `vici:"id"` -//} - -//type remoteOpts struct { -// Auth string `vici:"auth"` -//} - -//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"` -//} - -func removeSpace(s string) string { - rr := make([]rune, 0, len(s)) - for _, r := range s { - if !unicode.IsSpace(r) { - rr = append(rr, r) - } - } - return string(rr) -} - -func load_connection(command string, conn *pb.IPsecCreateRequest) error { - var sb strings.Builder - var tstr string - - // What has to happen here is: - // 1. Write charon config file for connection - // 2. Send vici command to reload-configuration - // 3. Send vici command to connect with the new connection - - // Create file name, removing spaces - no_space_sb := removeSpace(conn.Tunnel.Tunnels[0].GetName()) - saved_name := no_space_sb - no_space_sb = fmt.Sprintf("/etc/swanctl/%s.conf", no_space_sb) - log.Printf("Looking at file %s", no_space_sb) - - // Header - tstr = fmt.Sprintf("%s {\n", saved_name) - sb.WriteString(tstr) - - tstr = fmt.Sprintf(" remote_addrs = %s\n", conn.Tunnel.Tunnels[0].GetRemoteIp()) - sb.WriteString(tstr) - - tstr = " vips = 0.0.0.0\n" - sb.WriteString(tstr) - - tstr = " local {\n" - sb.WriteString(tstr) - - tstr = " auth = pubkey\n" - sb.WriteString(tstr) - - tstr = " certs = clientCert.pem\n" - sb.WriteString(tstr) - - tstr = " id = client.strongswan.org\n" - sb.WriteString(tstr) - - tstr = " }\n" - sb.WriteString(tstr) - - tstr = " remote {\n" - sb.WriteString(tstr) - - tstr = " auth = pubkey\n" - sb.WriteString(tstr) - - tstr = " id = server.strongswan.org\n" - sb.WriteString(tstr) - - tstr = " }\n" - sb.WriteString(tstr) - - tstr = " children {\n" - sb.WriteString(tstr) - - tstr = " net {\n" - sb.WriteString(tstr) - - tstr = " remote_ts = 10.1.0.0/16\n" - sb.WriteString(tstr) - - tstr = " esp_proposals = " - sb.WriteString(tstr) - - for i := 0; i < len(conn.Tunnel.Tunnels); i++ { - sb.WriteString(strings.ToLower(conn.Tunnel.Tunnels[i].GetCryptoAlg().String())) - tstr = "-" - sb.WriteString(tstr) - sb.WriteString(strings.ToLower(conn.Tunnel.Tunnels[i].GetIntegAlg().String())) - if (i+1) < len(conn.Tunnel.Tunnels) { - tstr = "-" - sb.WriteString(tstr) - } - } - - tstr = "\n" - sb.WriteString(tstr) - - tstr = " dpd_action = trap\n" - sb.WriteString(tstr) - - tstr = " }\n" - sb.WriteString(tstr) - - tstr = " host {\n" - sb.WriteString(tstr) - - tstr = " esp_proposals = " - sb.WriteString(tstr) - - for i := 0; i < len(conn.Tunnel.Tunnels); i++ { - sb.WriteString(strings.ToLower(conn.Tunnel.Tunnels[i].GetCryptoAlg().String())) - tstr = "-" - sb.WriteString(tstr) - sb.WriteString(strings.ToLower(conn.Tunnel.Tunnels[i].GetIntegAlg().String())) - if (i+1) < len(conn.Tunnel.Tunnels) { - tstr = "-" - sb.WriteString(tstr) - } - } - - tstr = "\n" - sb.WriteString(tstr) - - tstr = " dpd_action = trap\n" - sb.WriteString(tstr) - - tstr = " }\n" - sb.WriteString(tstr) - - tstr = " }\n" - sb.WriteString(tstr) - - tstr = " version = 2\n" - sb.WriteString(tstr) - - tstr = " proposals = " - sb.WriteString(tstr) - - for i := 0; i < len(conn.Sa.Sas); i++ { - sb.WriteString(strings.ReplaceAll(strings.ToLower(conn.Sa.Sas[i].GetCryptoAlg().String()), "_", "")) - tstr = "-" - sb.WriteString(tstr) - sb.WriteString(strings.ReplaceAll(strings.ToLower(conn.Sa.Sas[i].GetIntegAlg().String()), "_", "")) - if (i+1) < len(conn.Tunnel.Tunnels) { - tstr = "-" - sb.WriteString(tstr) - } - } - - tstr = "\n" - sb.WriteString(tstr) - - tstr = " dpd_delay = 60s\n" - sb.WriteString(tstr) - - - tstr = "}\n" - sb.WriteString(tstr) - - log.Printf("----- Config file -----") - log.Printf("\n%v\n", sb.String()) - log.Printf("-----------------------") - - // Check if file exists - if _, err := os.Stat(no_space_sb); errors.Is(err, os.ErrExist) { - return errors.New("Config file already exists") - } - - // Create file - f, err := os.Create(no_space_sb) - if err != nil { - return errors.New("Failed creating file") - } - defer f.Close() - - // Write config to file - n3, err := f.WriteString(sb.String()) - if err != nil { - return errors.New("Failed writing configuration file") - } - log.Printf("Wrote %d bytes to file", n3) - - err = f.Sync() - if err != nil { - log.Printf("Failed syncing file, soft eror") - } - - return nil -} - -func delete_connection(command string, conn *pb.IPsecCreateRequest) error { - no_space_sb := removeSpace(conn.Tunnel.Tunnels[0].GetName()) - no_space_sb = fmt.Sprintf("/etc/swanctl/%s.conf", no_space_sb) - log.Printf("Looking at file %s", no_space_sb) - - err := os.Remove(no_space_sb) - if err != nil { - return errors.New("Failed removing file") - } - - return nil -} diff --git a/server/ipsec_vici.go b/server/ipsec_vici.go new file mode 100644 index 0000000..d6d4057 --- /dev/null +++ b/server/ipsec_vici.go @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2022 Intel Corporation, or its subsidiaries. + +package main + +import ( + //"errors" + //"fmt" + "log" + //"os" + "strings" + //"unicode" + + "github.com/strongswan/govici/vici" + pb "github.com/opiproject/opi-api/security/proto" +) + +type childSA struct { + RemoteTrafficSelectors []string `vici:"remote_ts"` + LocalTrafficSelectors []string `vici:"local_ts"` + Updown string `vici:"updown"` + ESPProposals []string `vici:"esp_proposals"` +} + +type connection struct { + Name string // This field will NOT be marshaled! + + LocalAddrs []string `vici:"local_addrs"` + Local map[string]interface{} `vici:"local"` + Remote map[string]interface{} `vici:"remote"` + Children map[string]childSA `vici:"children"` + Version int `vici:"version"` + Proposals []string `vici:"proposals"` + Sendcertreq string `vici:"send_certreq"` +} + +func loadConn(connreq *pb.IPsecCreateRequest) error { + var ike_prop strings.Builder + var esp_prop strings.Builder + var tstr string + + // Setup IKE proposals + for i := 0; i < len(connreq.Sa.Sas); i++ { + ike_prop.WriteString(strings.ReplaceAll(strings.ToLower(connreq.Sa.Sas[i].GetCryptoAlg().String()), "_", "")) + tstr = "-" + ike_prop.WriteString(tstr) + ike_prop.WriteString(strings.ReplaceAll(strings.ToLower(connreq.Sa.Sas[i].GetIntegAlg().String()), "_", "")) + tstr = "-" + ike_prop.WriteString(tstr) + ike_prop.WriteString(strings.ToLower(connreq.Sa.Sas[i].GetDhgroups().String())) + if (i+1) < len(connreq.Sa.Sas) { + tstr = "-" + ike_prop.WriteString(tstr) + } + } + + // Setup child proposals + for i := 0; i < len(connreq.Tunnel.Tunnels); i++ { + esp_prop.WriteString(strings.ToLower(connreq.Tunnel.Tunnels[i].GetCryptoAlg().String())) + tstr = "-" + esp_prop.WriteString(tstr) + esp_prop.WriteString(strings.ToLower(connreq.Tunnel.Tunnels[i].GetIntegAlg().String())) + tstr = "-" + esp_prop.WriteString(tstr) + esp_prop.WriteString(strings.ToLower(connreq.Tunnel.Tunnels[i].GetDhgroups().String())) + if (i+1) < len(connreq.Tunnel.Tunnels) { + tstr = "-" + esp_prop.WriteString(tstr) + } + } + + // Build the connection object + conn := &connection { + Name: connreq.GetName(), + Local: map[string]interface{} { + "auth": "psk", + "id": "hacker@strongswan.org", + }, + Remote: map[string]interface{} { + "auth": "psk", + "id": "server.strongswan.org", + }, + Children: map[string]childSA{ + "net": childSA { + RemoteTrafficSelectors: []string {"10.1.0.0/16"}, + ESPProposals: []string {esp_prop.String()}, + }, + "host": childSA { + ESPProposals: []string {esp_prop.String()}, + }, + }, + Version: 2, + Proposals: []string {ike_prop.String()}, + Sendcertreq: "no", + } + + log.Printf("Built connection object: %v", conn) + for k, v := range conn.Children { + log.Printf("key[%s] value[%s]\n", k, v) + } + log.Printf("Local: %v", conn.Local) + log.Printf("Remote: %v", conn.Remote) + + s, err := vici.NewSession() + if err != nil { + log.Printf("Failed creating vici session") + return err + } + defer s.Close() + + c, err := vici.MarshalMessage(conn) + if err != nil { + log.Printf("Failed marshalling message") + return err + } + + m := vici.NewMessage() + if err := m.Set(conn.Name, c); err != nil { + log.Printf("Failed setting command") + return err + } + + _, err = s.CommandRequest("load-conn", m) + + log.Printf("command error return [%v]", err) + + return err +} + +/* +func initiate_connection(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.Messages() { + if err := msg.Err(); err != nil { + return err + } + } + + return nil +} +*/