Skip to content

openziti/sdk-golang

Repository files navigation

Ziggy using the sdk-golang

Ziti SDK for Golang

The OpenZiti SDK for GoLang allows developers to create their own custom OpenZiti network endpoint clients and management tools. OpenZiti is a modern, programmable network overlay with associated edge components, for application-embedded, zero trust network connectivity, written by developers for developers. The SDK harnesses that power via APIs that allow developers to imagine and develop solutions beyond what OpenZiti handles by default.

This SDK does the following:

Table of Contents

Important Packages

This repository has a number of different folders, however below are the most important ones for a new developer to be aware of.

  • ziti - the main SDK package that will be included in your project
  • edge-apis - provides low-level abstractions for authenticating and accessing the Ziti Edge Client and Management APIs
  • example - various example applications that illustrate different uses of the SDK. Each example contains its own README.md.
    • chat - a bare-bones example of a client and server for a chat program over an OpenZiti Service
    • chat-p2p - highlights addressable terminators which allows clients to dial specific services hosts if there are multiple hosts
    • curlz - wrapping existing network tooling (curl) to work over OpenZiti
    • grpc-example - using GRPC over OpenZiti
    • http-client - a HTTP client accessing a web server over HTTP
    • jwtchat - highlights using external JWTs ( from OIDC/oAuth/etc.) to authenticate with OpenZIti
    • reflect - a low level network "echo" client and server example
    • simple-server - a bare-bones HTTP server side only example
    • udp-offload - an example demonstrating how to work with an OpenZiti client and a UDP server
    • zcat - wrapping existing network tooling (netcat) to work over OpenZiti
    • zping - wrapping existing network tooling (ping) to work over OpenZiti

Writing Your Own Endpoint Client

An "endpoint client" in OpenZiti's language is an identity that is dialing (accessing) or binding (hosting) a service. Dialing contacts either another identity hosting a service, which may be another client endpoint, or it may be handled by an Edge Router depending on its termination configuration. This SDK supports binding and dialing, which means it can host or access services depending on what it is instructed to do and the policies affecting the software's identity and service(s).

To test a client endpoint you will need the following outside your normal Golang development environment:

  1. An OpenZiti Network with controller with at least one Edge Router (See Quick Starts)
  2. A service to dial (access) and bind (host) (See Allowing Dial/Bind Access To A Service)
  3. An identity for your client to test with (See Creating & Enrolling a Dial Identity)

The steps for writing any endpoint client are:

  1. Load/Create a configuration
  2. Create a instance
  3. Dial/Bind a service

The above links provide the steps in more detail, but here is the most basic setup to dial a service with most error handling removed for brevity:

	cfg, _ := ziti.NewConfigFromFile("client.json")
	
	context, _ := ziti.NewContext(cfg)
	
	conn, _ := context.Dial(serviceName)
	
	if _, err := conn.Write([]byte("hello I am myTestClient")); err != nil {
		panic(err)
	}

Load/Create A Configuration

Configuration can be done through a file or through code that creates a Config instance. Loading through a file support x509 authentication only while creating custom Config instances allows for all authentication methods (x509, Username/Password, JWT, etc.).

The easiest way to create a configuration is by using the ziti edge enroll capabilities that will generate an identity file that provides the location of the OpenZiti controller, the configuration types the client is interested in, and the x509 certificate and private key to use.

Example: File Configuration

cfg, err := ziti.NewConfigFromFile("client.json")
if err != nil {
    _, _ = fmt.Fprintf(os.Stderr, "failed to read configuration: %v", err)
    os.Exit(1)
}

Example: Code Configuration

// Note that GetControllerWellKnownCaPool() does not verify the authenticity of the controller, it is assumed
// this is handled in some other way.
caPool, err := ziti.GetControllerWellKnownCaPool("https://localhost:1280")

if err != nil {
    panic(err)
}

credentials := edge_apis.NewUpdbCredentials("Joe Admin", "20984hgn2q048ngq20-3gn")
credentials.CaPool = caPool

cfg := &ziti.Config{
    ZtAPI:       "https://localhost:1280/edge/client/v1",
    Credentials: credentials,
}
ctx, err := ziti.NewContext(cfg)

Create A Ziti Context

A Context instances represent a specific identity connected to a Ziti Controller. The instance, once configured, will handle authentication, re-authentication, posture state submission, and provides interfaces to dial/bind services.

context, err := ziti.NewContext(cfg)

if err != nil {
    _, _ = fmt.Fprintf(os.Stderr, "failed to create context: %v", err)
    os.Exit(1)
}

Dial/Bind A Service

The main activity performed with a Context is to dial or bind a service. In order for a dial or bind to be successful, the following must be true:

  1. The identity must have the proper dial or bind service policy to the service via Service Policies
  2. The identity must have the proper dial or bind services over at least one Edge Router via Edge Router Policies
  3. The service must be allowed to be dialed or bound on at least one Edge Router via Service Edge Router Policies)

The easiest way to satisfy #2 and #3 are the make use of the #all role attribute when creating the policies. Edge Router policies and Service Edge Router Policies are useful for geographic connection management. For smaller networks, test networks, and networks without geographic network entry are not concerns they add complexity without inherent benefit. Using the #all role attributes makes all service accessible and valid dial/bind targets on all Edge Routers.

Example: "All" Edge Router and Service Edge Router Policies

> ziti edge create service-edge-router-policy serp-all --edge-router-roles "#all" --service-roles "#all"
> ziti edge create edge-router-policy erp-all --edge-router-roles "#all" --identity-roles "#all"

Example: Dial and Bind Policies For a Service

> ziti edge create service-policy  testDial Dial --identity-roles "@myTestClient" --service-roles "@myChat"
> ziti edge create service-policy  testBind Bind --identity-roles "@myTestServer" --service-roles "@myChat"

Note: While policies can be created targeting specific users, services, or routers, using #attribute style assignments allows you to grant access based on groupings. (See Roles and Role Attributes)

Example: Dial

conn, err := context.Dial(serviceName)

if err != nil {
    _, _ = fmt.Fprintf(os.Stderr, "failed to dial service %v, err: %+v\n", serviceName, err)
    os.Exit(1)
}

if _, err := conn.Write([]byte("hello I am myTestClient")); err != nil {
    panic(err)
}

Example: Bind

Note: A full implementation will have to accept connections, hand them off to another goroutine and then re-wait on listener.Accept()

func main(){
    //... load configuration, create context
    
    listener, err := context.ListenWithOptions(serviceName, &options)
    if err != nil {
        logrus.Errorf("Error binding service %+v", err)
        panic(err)
    }
    
    for {
        conn, err := listener.Accept()
        if err != nil {
            logger.Errorf("server error, exiting: %+v\n", err)
            panic(err)
        }
        logger.Infof("new connection")
        go handleConn(conn)
    }
}

func handleConn(conn net.Conn){
    for {
        buf := make([]byte, 1024)
        n, err := conn.Read(buf)
        if err != nil {
            _ = conn.Close()
            return
        }
        stringData := string(buf[:n])
        println(stringData)
    }
}

Creating & Enrolling an Identity

For more detail on how to create and enroll identities see the identities section in the OpenZiti documentation.

  1. Login to the controller ziti edge login https://ctrl-api/edge/client/v1 -u <username> -p <password>
  2. Create a new identity ziti edge create identity device myTestClient -o client.enroll.jwt
  3. Enroll the identity ziti edge enroll client.enroll.jwt -o client.json

The output file, client.json in this file, is used as that target in the SDK call ziti.NewConfigFromFile("client.json") to create a configuration.

Allowing Dial/Bind Access to a Service

For more detail on policies see the policies section in the OpenZiti documentation.

  1. Login if not already logged in ziti edge login https://ctrl-api/edge/client/v1 -u <username> -p <password>
  2. Create a new service ziti edge create service myChat
  3. Allow the service to be accessed by the myTestClient through any Edge Router and the service myChat through any Edge Router
    1. ziti edge create service-policy testPolicy Dial --identity-roles "@myTestClient" --service-roles "@myChat"
    2. ziti edge create service-edge-router-policy chatOverAll --edge-router-roles "#all" --service-roles "@myChat"

Note: While policies can be created targeting specific users, services, or routers, using #attribute style assignments allows you to grant access based on groupings. (See Roles and Role Attributes)

Accessing the Management/Client API

The Edge Management and Client APIs are defined by an OpenAPI 2.0 specification and have a client that is generated and maintained in another GitHub repository. Accessing this repository directly should not be necessary. This SDK provides a wrapper around the generated clients found in edge-apis.

Example: Creating an Edge Management API Client

apiUrl, _ = url.Parse("https://localhost:1280/edge/management/v1") 

// Note that GetControllerWellKnownCaPool() does not verify the authenticity of the controller, it is assumed
// this is handled in some other way.
caPool, err := ziti.GetControllerWellKnownCaPool("https://localhost:1280")

if err != nil {
panic(err)
}

credentials := edge_apis.NewUpdbCredentials("Joe Admin", "20984hgn2q048ngq20-3gn")
credentials.CaPool = caPool

//Note: the CA pool can be provided here or during the Authenticate(<creds>) call. It is allowed here to enable
//      calls to REST API endpoints that do not require authentication.
managementClient := edge_apis.NewManagementApiClient(apiUrl, credentials.GetCaPool()),

//"configTypes" are string identifiers of configuration that can be requested by clients. Developers may
//specify their own in order to provide distributed identity and/or service specific configurations.
//
//See: https://openziti.io/docs/learn/core-concepts/config-store/overview
//Example: configTypes = []string{"myCustomAppConfigType"}
var configTypes []string

apiSesionDetial, err := managementClient.Authenticate(credentials, configTypes)

Example: Creating an Edge Client API Client

apiUrl, _ = url.Parse("https://localhost:1280/edge/client/v1") 

// Note that GetControllerWellKnownCaPool() does not verify the authenticity of the controller, it is assumed
// this is handled in some other way.
caPool, err := ziti.GetControllerWellKnownCaPool("https://localhost:1280")

if err != nil {
panic(err)
}

credentials := edge_apis.NewUpdbCredentials("Joe Admin", "20984hgn2q048ngq20-3gn")
credentials.CaPool = caPool

//Note: the CA pool can be provided here or during the Authenticate(<creds>) call. It is allowed here to enable
//      calls to REST API endpoints that do not require authentication.
client := edge_apis.NewClientApiClient(apiUrl, credentials.GetCaPool()),

//"configTypes" are string identifiers of configuration that can be requested by clients. Developers may
//specify their own in order to provide distributed identity and/or service specific configurations. The
//OpenZiti tunnelers use this capability to configure interception of network connections.
//See: https://openziti.io/docs/learn/core-concepts/config-store/overview
//Example: configTypes = []string{"myCustomAppConfigType"}
var configTypes []string

apiSesionDetial, err := client.Authenticate(credentials, configTypes)

Example: Requesting Management Services

The following example show how to list services. Altering the names of the package types used will allow the same code to work for the Edge Client API.

// GetServices retrieves services in chunks of 500 till it has accumulated all services.
func GetServices(client *apis.ManagementApiClient) ([]*rest_model.ServiceDetail, error) {
	params := service.NewListServicesParams()

	pageOffset := int64(0)
	pageLimit := int64(500)

	var services []*rest_model.ServiceDetail

	for {
		params.Limit = &pageLimit
		params.Offset = &pageOffset

		resp, err := client.API.Service.ListServices(params, nil)

		if err != nil {
			return nil, rest_util.WrapErr(err)
		}

		if services == nil {
			services = make([]*rest_model.ServiceDetail, 0, *resp.Payload.Meta.Pagination.TotalCount)
		}

		services = append(services, resp.Payload.Data...)

		pageOffset += pageLimit
		if pageOffset >= *resp.Payload.Meta.Pagination.TotalCount {
			break
		}
	}

	return services, nil
}