mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c7cf688e6 | |||
| a39d95d5f7 | |||
| 01ad2785ee | |||
| 6822e4f8ab | |||
| 834c0eaeed | |||
| 74a3026963 | |||
| 8445b88d3c | |||
| 7a55208c61 | |||
| 581cfb8480 | |||
| 201c462902 | |||
| ebae7a7024 | |||
| 70e675f42c | |||
| 8f46065ab5 | |||
| 41b9c22216 | |||
| 88ce63e785 | |||
| 2dc5f6ec8c |
+12
@@ -1,5 +1,17 @@
|
||||
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
|
||||
|
||||
## 2021.12.2
|
||||
### Bug Fixes
|
||||
- Fix logging when `quic` transport is used and UDP traffic is proxied.
|
||||
- FIPS compliant cloudflared binaries will now be released as separate artifacts. Recall that these are only for linux
|
||||
and amd64.
|
||||
|
||||
## 2021.12.1
|
||||
### Bug Fixes
|
||||
- Fixes Github issue #530 where cloudflared 2021.12.0 could not reach origins that were HTTPS and using certain encryption
|
||||
methods forbidden by FIPS compliance (such as Let's Encrypt certificates). To address this fix we have temporarily reverted
|
||||
FIPS compliance from amd64 linux binaries that was recently introduced (or fixed actually as it was never working before).
|
||||
|
||||
## 2021.12.0
|
||||
### New Features
|
||||
- Cloudflared binary released for amd64 linux is now FIPS compliant.
|
||||
|
||||
@@ -3,11 +3,18 @@ MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut
|
||||
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
||||
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
|
||||
|
||||
ifeq ($(FIPS), true)
|
||||
BINARY_NAME := cloudflared-fips
|
||||
else
|
||||
BINARY_NAME := cloudflared
|
||||
endif
|
||||
|
||||
ifeq ($(NIGHTLY), true)
|
||||
# We do not release FIPS in NIGHTLY, so no need to consider that case here.
|
||||
DEB_PACKAGE_NAME := cloudflared-nightly
|
||||
NIGHTLY_FLAGS := --conflicts cloudflared --replaces cloudflared
|
||||
else
|
||||
DEB_PACKAGE_NAME := cloudflared
|
||||
DEB_PACKAGE_NAME := $(BINARY_NAME)
|
||||
endif
|
||||
|
||||
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
|
||||
@@ -18,6 +25,7 @@ ifeq ($(FIPS), true)
|
||||
LINK_FLAGS := -linkmode=external -extldflags=-static $(LINK_FLAGS)
|
||||
# Prevent linking with libc regardless of CGO enabled or not.
|
||||
GO_BUILD_TAGS := $(GO_BUILD_TAGS) osusergo netgo fips
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "main.BuildType=FIPS"
|
||||
endif
|
||||
|
||||
LDFLAGS := -ldflags='$(VERSION_FLAGS) $(LINK_FLAGS)'
|
||||
@@ -65,9 +73,9 @@ else
|
||||
endif
|
||||
|
||||
ifeq ($(TARGET_OS), windows)
|
||||
EXECUTABLE_PATH=./cloudflared.exe
|
||||
EXECUTABLE_PATH=./$(BINARY_NAME).exe
|
||||
else
|
||||
EXECUTABLE_PATH=./cloudflared
|
||||
EXECUTABLE_PATH=./$(BINARY_NAME)
|
||||
endif
|
||||
|
||||
ifeq ($(FLAVOR), centos-7)
|
||||
@@ -114,10 +122,10 @@ test-ssh-server:
|
||||
docker-compose -f ssh_server_tests/docker-compose.yml up
|
||||
|
||||
define publish_package
|
||||
chmod 664 cloudflared*.$(1); \
|
||||
chmod 664 $(BINARY_NAME)*.$(1); \
|
||||
for HOST in $(CF_PKG_HOSTS); do \
|
||||
ssh-keyscan -t ecdsa $$HOST >> ~/.ssh/known_hosts; \
|
||||
scp -p -4 cloudflared*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/cloudflared/; \
|
||||
scp -p -4 $(BINARY_NAME)*.$(1) cfsync@$$HOST:/state/cf-pkg/staging/$(2)/$(TARGET_PUBLIC_REPO)/$(BINARY_NAME)/; \
|
||||
done
|
||||
endef
|
||||
|
||||
@@ -129,6 +137,8 @@ publish-deb: cloudflared-deb
|
||||
publish-rpm: cloudflared-rpm
|
||||
$(call publish_package,rpm,yum)
|
||||
|
||||
# When we build packages, the package name will be FIPS-aware.
|
||||
# But we keep the binary installed by it to be named "cloudflared" regardless.
|
||||
define build_package
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
@@ -147,6 +157,10 @@ endef
|
||||
cloudflared-deb: cloudflared
|
||||
$(call build_package,deb)
|
||||
|
||||
.PHONY: cloudflared-internal-deb
|
||||
cloudflared-internal-deb: cloudflared-deb
|
||||
bash -c 'for f in cloudflared-fips_*.deb; do mv -- "$$f" "$${f/-fips/}"; done'
|
||||
|
||||
.PHONY: cloudflared-rpm
|
||||
cloudflared-rpm: cloudflared
|
||||
$(call build_package,rpm)
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
2021.12.4
|
||||
- 2021-12-27 TUN-5482: Refactor tunnelstore client related packages for more coherent package
|
||||
- 2021-12-27 TUN-5551: Change internally published debian package to be FIPS compliant
|
||||
- 2021-12-27 TUN-5551: Show whether the binary was built for FIPS compliance
|
||||
|
||||
2021.12.3
|
||||
- 2021-12-22 TUN-5584: Changes for release 2021.12.2
|
||||
- 2021-12-22 TUN-5590: QUIC datagram max user payload is 1217 bytes
|
||||
- 2021-12-22 TUN-5593: Read full packet from UDP connection, even if it exceeds MTU of the transport. When packet length is greater than the MTU of the transport, we will silently drop packets (for now).
|
||||
- 2021-12-23 TUN-5597: Log session ID when session is terminated by edge
|
||||
|
||||
2021.12.2
|
||||
- 2021-12-20 TUN-5571: Remove redundant session manager log, it's already logged in origin/tunnel.ServeQUIC
|
||||
- 2021-12-20 TUN-5570: Only log RPC server events at error level to reduce noise
|
||||
- 2021-12-14 TUN-5494: Send a RPC with terminate reason to edge if the session is closed locally
|
||||
- 2021-11-09 TUN-5551: Reintroduce FIPS compliance for linux amd64 now as separate binaries
|
||||
|
||||
2021.12.1
|
||||
- 2021-12-16 TUN-5549: Revert "TUN-5277: Ensure cloudflared binary is FIPS compliant on linux amd64"
|
||||
|
||||
2021.12.0
|
||||
- 2021-12-13 TUN-5530: Get current time from ticker
|
||||
- 2021-12-15 TUN-5544: Update CHANGES.md for next release
|
||||
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
# This controls the directory the built artifacts go into
|
||||
export ARTIFACT_DIR=built_artifacts/
|
||||
mkdir -p $ARTIFACT_DIR
|
||||
|
||||
arch=("amd64")
|
||||
export TARGET_ARCH=$arch
|
||||
export TARGET_OS=linux
|
||||
export FIPS=true
|
||||
# For BoringCrypto to link, we need CGO enabled. Otherwise compilation fails.
|
||||
export CGO_ENABLED=1
|
||||
|
||||
make cloudflared-deb
|
||||
mv cloudflared-fips\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-fips-linux-$arch.deb
|
||||
|
||||
# rpm packages invert the - and _ and use x86_64 instead of amd64.
|
||||
RPMVERSION=$(echo $VERSION|sed -r 's/-/_/g')
|
||||
RPMARCH="x86_64"
|
||||
make cloudflared-rpm
|
||||
mv cloudflared-fips-$RPMVERSION-1.$RPMARCH.rpm $ARTIFACT_DIR/cloudflared-fips-linux-$RPMARCH.rpm
|
||||
|
||||
# finally move the linux binary as well.
|
||||
mv ./cloudflared $ARTIFACT_DIR/cloudflared-fips-linux-$arch
|
||||
+2
-8
@@ -1,4 +1,4 @@
|
||||
VERSION=$(git describe --tags --always --dirty="-dev" --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
VERSION=$(git describe --tags --always --match "[0-9][0-9][0-9][0-9].*.*")
|
||||
echo $VERSION
|
||||
|
||||
# Avoid depending on C code since we don't need it.
|
||||
@@ -17,15 +17,9 @@ for arch in ${windowsArchs[@]}; do
|
||||
done
|
||||
|
||||
|
||||
# amd64 is last because we override settings for it
|
||||
linuxArchs=("386" "arm" "arm64" "amd64")
|
||||
linuxArchs=("386" "amd64" "arm" "arm64")
|
||||
export TARGET_OS=linux
|
||||
for arch in ${linuxArchs[@]}; do
|
||||
if [ "${arch}" = "amd64" ]; then
|
||||
export FIPS=true
|
||||
# For BoringCrypto to link, we need CGO enabled. Otherwise compilation fails.
|
||||
export CGO_ENABLED=1
|
||||
fi
|
||||
export TARGET_ARCH=$arch
|
||||
make cloudflared-deb
|
||||
mv cloudflared\_$VERSION\_$arch.deb $ARTIFACT_DIR/cloudflared-linux-$arch.deb
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 15 * time.Second
|
||||
jsonContentType = "application/json"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrBadRequest = errors.New("incorrect request parameters")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrAPINoSuccess = errors.New("API call failed")
|
||||
)
|
||||
|
||||
type RESTClient struct {
|
||||
baseEndpoints *baseEndpoints
|
||||
authToken string
|
||||
userAgent string
|
||||
client http.Client
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
type baseEndpoints struct {
|
||||
accountLevel url.URL
|
||||
zoneLevel url.URL
|
||||
accountRoutes url.URL
|
||||
accountVnets url.URL
|
||||
}
|
||||
|
||||
var _ Client = (*RESTClient)(nil)
|
||||
|
||||
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
||||
}
|
||||
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
|
||||
}
|
||||
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
httpTransport := http.Transport{
|
||||
TLSHandshakeTimeout: defaultTimeout,
|
||||
ResponseHeaderTimeout: defaultTimeout,
|
||||
}
|
||||
http2.ConfigureTransport(&httpTransport)
|
||||
return &RESTClient{
|
||||
baseEndpoints: &baseEndpoints{
|
||||
accountLevel: *accountLevelEndpoint,
|
||||
zoneLevel: *zoneLevelEndpoint,
|
||||
accountRoutes: *accountRoutesEndpoint,
|
||||
accountVnets: *accountVnetsEndpoint,
|
||||
},
|
||||
authToken: authToken,
|
||||
userAgent: userAgent,
|
||||
client: http.Client{
|
||||
Transport: &httpTransport,
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
if bodyBytes, err := json.Marshal(body); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to serialize json body")
|
||||
} else {
|
||||
bodyReader = bytes.NewBuffer(bodyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url.String(), bodyReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "can't create %s request", method)
|
||||
}
|
||||
req.Header.Set("User-Agent", r.userAgent)
|
||||
if bodyReader != nil {
|
||||
req.Header.Set("Content-Type", jsonContentType)
|
||||
}
|
||||
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
||||
req.Header.Add("Accept", "application/json;version=1")
|
||||
return r.client.Do(req)
|
||||
}
|
||||
|
||||
func parseResponse(reader io.Reader, data interface{}) error {
|
||||
// Schema for Tunnelstore responses in the v1 API.
|
||||
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
|
||||
var result response
|
||||
// First, parse the wrapper and check the API call succeeded
|
||||
if err := json.NewDecoder(reader).Decode(&result); err != nil {
|
||||
return errors.Wrap(err, "failed to decode response")
|
||||
}
|
||||
if err := result.checkErrors(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !result.Success {
|
||||
return ErrAPINoSuccess
|
||||
}
|
||||
// At this point we know the API call succeeded, so, parse out the inner
|
||||
// result into the datatype provided as a parameter.
|
||||
if err := json.Unmarshal(result.Result, &data); err != nil {
|
||||
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []apiErr `json:"errors,omitempty"`
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
func (r *response) checkErrors() error {
|
||||
if len(r.Errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(r.Errors) == 1 {
|
||||
return r.Errors[0]
|
||||
}
|
||||
var messages string
|
||||
for _, e := range r.Errors {
|
||||
messages += fmt.Sprintf("%s; ", e)
|
||||
}
|
||||
return fmt.Errorf("API errors: %s", messages)
|
||||
}
|
||||
|
||||
type apiErr struct {
|
||||
Code json.Number `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (e apiErr) Error() string {
|
||||
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
|
||||
if resp.Header.Get("Content-Type") == "application/json" {
|
||||
var errorsResp response
|
||||
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
|
||||
if err := errorsResp.checkErrors(); err != nil {
|
||||
return errors.Errorf("Failed to %s: %s", op, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusBadRequest:
|
||||
return ErrBadRequest
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return ErrUnauthorized
|
||||
case http.StatusNotFound:
|
||||
return ErrNotFound
|
||||
}
|
||||
return errors.Errorf("API call to %s failed with status %d: %s", op,
|
||||
resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TunnelClient interface {
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID) error
|
||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
||||
}
|
||||
|
||||
type HostnameClient interface {
|
||||
RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error)
|
||||
}
|
||||
|
||||
type IPRouteClient interface {
|
||||
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
|
||||
AddRoute(newRoute NewRoute) (Route, error)
|
||||
DeleteRoute(params DeleteRouteParams) error
|
||||
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
|
||||
}
|
||||
|
||||
type VnetClient interface {
|
||||
CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error)
|
||||
ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error)
|
||||
DeleteVirtualNetwork(id uuid.UUID) error
|
||||
UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
TunnelClient
|
||||
HostnameClient
|
||||
IPRouteClient
|
||||
VnetClient
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Change = string
|
||||
|
||||
const (
|
||||
ChangeNew = "new"
|
||||
ChangeUpdated = "updated"
|
||||
ChangeUnchanged = "unchanged"
|
||||
)
|
||||
|
||||
// HostnameRoute represents a record type that can route to a tunnel
|
||||
type HostnameRoute interface {
|
||||
json.Marshaler
|
||||
RecordType() string
|
||||
UnmarshalResult(body io.Reader) (HostnameRouteResult, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
type HostnameRouteResult interface {
|
||||
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
|
||||
SuccessSummary() string
|
||||
}
|
||||
|
||||
type DNSRoute struct {
|
||||
userHostname string
|
||||
overwriteExisting bool
|
||||
}
|
||||
|
||||
type DNSRouteResult struct {
|
||||
route *DNSRoute
|
||||
CName Change `json:"cname"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewDNSRoute(userHostname string, overwriteExisting bool) HostnameRoute {
|
||||
return &DNSRoute{
|
||||
userHostname: userHostname,
|
||||
overwriteExisting: overwriteExisting,
|
||||
}
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
UserHostname string `json:"user_hostname"`
|
||||
OverwriteExisting bool `json:"overwrite_existing"`
|
||||
}{
|
||||
Type: dr.RecordType(),
|
||||
UserHostname: dr.userHostname,
|
||||
OverwriteExisting: dr.overwriteExisting,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||
var result DNSRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = dr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) RecordType() string {
|
||||
return "dns"
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) String() string {
|
||||
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
|
||||
}
|
||||
|
||||
func (res *DNSRouteResult) SuccessSummary() string {
|
||||
var msgFmt string
|
||||
switch res.CName {
|
||||
case ChangeNew:
|
||||
msgFmt = "Added CNAME %s which will route to this tunnel"
|
||||
case ChangeUpdated: // this is not currently returned by tunnelsore
|
||||
msgFmt = "%s updated to route to your tunnel"
|
||||
case ChangeUnchanged:
|
||||
msgFmt = "%s is already configured to route to your tunnel"
|
||||
}
|
||||
return fmt.Sprintf(msgFmt, res.hostname())
|
||||
}
|
||||
|
||||
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
|
||||
// requested name is returned instead (should not be the common path, it is just a fall-back).
|
||||
func (res *DNSRouteResult) hostname() string {
|
||||
if res.Name != "" {
|
||||
return res.Name
|
||||
}
|
||||
return res.route.userHostname
|
||||
}
|
||||
|
||||
type LBRoute struct {
|
||||
lbName string
|
||||
lbPool string
|
||||
}
|
||||
|
||||
type LBRouteResult struct {
|
||||
route *LBRoute
|
||||
LoadBalancer Change `json:"load_balancer"`
|
||||
Pool Change `json:"pool"`
|
||||
}
|
||||
|
||||
func NewLBRoute(lbName, lbPool string) HostnameRoute {
|
||||
return &LBRoute{
|
||||
lbName: lbName,
|
||||
lbPool: lbPool,
|
||||
}
|
||||
}
|
||||
|
||||
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
LBName string `json:"lb_name"`
|
||||
LBPool string `json:"lb_pool"`
|
||||
}{
|
||||
Type: lr.RecordType(),
|
||||
LBName: lr.lbName,
|
||||
LBPool: lr.lbPool,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) RecordType() string {
|
||||
return "lb"
|
||||
}
|
||||
|
||||
func (lb *LBRoute) String() string {
|
||||
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||
var result LBRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = lr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (res *LBRouteResult) SuccessSummary() string {
|
||||
var msg string
|
||||
switch res.LoadBalancer + "," + res.Pool {
|
||||
case "new,new":
|
||||
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
|
||||
case "new,updated":
|
||||
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
|
||||
case "new,unchanged":
|
||||
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
|
||||
case "updated,new":
|
||||
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
|
||||
case "updated,updated":
|
||||
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
|
||||
case "updated,unchanged":
|
||||
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
|
||||
case "unchanged,updated":
|
||||
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
|
||||
case "unchanged,unchanged":
|
||||
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
|
||||
case "unchanged,new":
|
||||
// this state is not possible
|
||||
fallthrough
|
||||
default:
|
||||
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
|
||||
}
|
||||
|
||||
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error) {
|
||||
endpoint := r.baseEndpoints.zoneLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
|
||||
resp, err := r.sendRequest("PUT", endpoint, route)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return route.UnmarshalResult(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
@@ -1,17 +1,9 @@
|
||||
package tunnelstore
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -105,125 +97,3 @@ func TestLBRouteResultSuccessSummary(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, actual, "case %d", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseListTunnels(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []*Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": []}`},
|
||||
want: []*Tunnel{},
|
||||
},
|
||||
{
|
||||
name: "success is false",
|
||||
args: args{body: `{"success": false, "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors are present",
|
||||
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid response",
|
||||
args: args{body: `abc`},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
|
||||
got, err := parseListTunnels(body)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unmarshalTunnel(t *testing.T) {
|
||||
type args struct {
|
||||
reader io.Reader
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := unmarshalTunnel(tt.args.reader)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelOk(t *testing.T) {
|
||||
|
||||
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
|
||||
expected := Tunnel{
|
||||
ID: uuid.Nil,
|
||||
Name: "test",
|
||||
CreatedAt: time.Time{},
|
||||
Connections: []Connection{},
|
||||
}
|
||||
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &expected, actual)
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelErr(t *testing.T) {
|
||||
|
||||
tests := []string{
|
||||
`abc`,
|
||||
`{"success": true, "result": abc}`,
|
||||
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
|
||||
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalConnections(t *testing.T) {
|
||||
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
|
||||
expected := ActiveClient{
|
||||
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
|
||||
Features: []string{"ha-origin"},
|
||||
Version: "2021.2.5",
|
||||
Arch: "darwin_amd64",
|
||||
RunAt: time.Time{},
|
||||
Connections: []Connection{{
|
||||
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
|
||||
ColoName: "LIS",
|
||||
IsPendingReconnect: false,
|
||||
OriginIP: net.ParseIP("148.38.28.2"),
|
||||
OpenedAt: time.Time{},
|
||||
}},
|
||||
}
|
||||
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package teamnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -133,3 +137,104 @@ type GetRouteByIpParams struct {
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
VNetID *uuid.UUID
|
||||
}
|
||||
|
||||
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
||||
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListDetailedRoutes(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list routes", resp)
|
||||
}
|
||||
|
||||
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
||||
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
|
||||
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
||||
if err != nil {
|
||||
return Route{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseRoute(resp.Body)
|
||||
}
|
||||
|
||||
return Route{}, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
|
||||
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
||||
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
_, err := parseRoute(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return r.statusCodeToError("delete route", resp)
|
||||
}
|
||||
|
||||
// GetByIP checks which route will proxy a given IP.
|
||||
func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseDetailedRoute(resp.Body)
|
||||
}
|
||||
|
||||
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
||||
}
|
||||
|
||||
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
|
||||
var routes []*DetailedRoute
|
||||
err := parseResponse(body, &routes)
|
||||
return routes, err
|
||||
}
|
||||
|
||||
func parseRoute(body io.ReadCloser) (Route, error) {
|
||||
var route Route
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
|
||||
var route DetailedRoute
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain
|
||||
// virtual network (if one is provided).
|
||||
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
|
||||
queryParams := url.Values{}
|
||||
if vnetID != nil {
|
||||
queryParams.Set("virtual_network_id", vnetID.String())
|
||||
}
|
||||
endpoint.RawQuery = queryParams.Encode()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package teamnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -13,90 +13,90 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
filterDeleted = cli.BoolFlag{
|
||||
filterIpRouteDeleted = cli.BoolFlag{
|
||||
Name: "filter-is-deleted",
|
||||
Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.",
|
||||
}
|
||||
filterTunnelID = cli.StringFlag{
|
||||
filterIpRouteTunnelID = cli.StringFlag{
|
||||
Name: "filter-tunnel-id",
|
||||
Usage: "Show only routes with the given tunnel ID.",
|
||||
}
|
||||
filterSubset = cli.StringFlag{
|
||||
filterSubsetIpRoute = cli.StringFlag{
|
||||
Name: "filter-network-is-subset-of",
|
||||
Aliases: []string{"nsub"},
|
||||
Usage: "Show only routes whose network is a subset of the given network.",
|
||||
}
|
||||
filterSuperset = cli.StringFlag{
|
||||
filterSupersetIpRoute = cli.StringFlag{
|
||||
Name: "filter-network-is-superset-of",
|
||||
Aliases: []string{"nsup"},
|
||||
Usage: "Show only routes whose network is a superset of the given network.",
|
||||
}
|
||||
filterComment = cli.StringFlag{
|
||||
filterIpRouteComment = cli.StringFlag{
|
||||
Name: "filter-comment-is",
|
||||
Usage: "Show only routes with this comment.",
|
||||
}
|
||||
filterVnet = cli.StringFlag{
|
||||
filterIpRouteByVnet = cli.StringFlag{
|
||||
Name: "filter-virtual-network-id",
|
||||
Usage: "Show only routes that are attached to the given virtual network ID.",
|
||||
}
|
||||
|
||||
// Flags contains all filter flags.
|
||||
FilterFlags = []cli.Flag{
|
||||
&filterDeleted,
|
||||
&filterTunnelID,
|
||||
&filterSubset,
|
||||
&filterSuperset,
|
||||
&filterComment,
|
||||
&filterVnet,
|
||||
IpRouteFilterFlags = []cli.Flag{
|
||||
&filterIpRouteDeleted,
|
||||
&filterIpRouteTunnelID,
|
||||
&filterSubsetIpRoute,
|
||||
&filterSupersetIpRoute,
|
||||
&filterIpRouteComment,
|
||||
&filterIpRouteByVnet,
|
||||
}
|
||||
)
|
||||
|
||||
// Filter which routes get queried.
|
||||
type Filter struct {
|
||||
// IpRouteFilter which routes get queried.
|
||||
type IpRouteFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
// NewFromCLI parses CLI flags to discover which filters should get applied.
|
||||
func NewFromCLI(c *cli.Context) (*Filter, error) {
|
||||
f := &Filter{
|
||||
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
|
||||
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||
f := &IpRouteFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
|
||||
// Set deletion filter
|
||||
if flag := filterDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||
f.deleted()
|
||||
} else {
|
||||
f.notDeleted()
|
||||
}
|
||||
|
||||
if subset, err := cidrFromFlag(c, filterSubset); err != nil {
|
||||
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if subset != nil {
|
||||
f.networkIsSupersetOf(*subset)
|
||||
}
|
||||
|
||||
if superset, err := cidrFromFlag(c, filterSuperset); err != nil {
|
||||
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if superset != nil {
|
||||
f.networkIsSupersetOf(*superset)
|
||||
}
|
||||
|
||||
if comment := c.String(filterComment.Name); comment != "" {
|
||||
if comment := c.String(filterIpRouteComment.Name); comment != "" {
|
||||
f.commentIs(comment)
|
||||
}
|
||||
|
||||
if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" {
|
||||
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
|
||||
u, err := uuid.Parse(tunnelID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterTunnelID.Name)
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
|
||||
}
|
||||
f.tunnelID(u)
|
||||
}
|
||||
|
||||
if vnetId := c.String(filterVnet.Name); vnetId != "" {
|
||||
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
|
||||
u, err := uuid.Parse(vnetId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterVnet.Name)
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
|
||||
}
|
||||
f.vnetID(u)
|
||||
}
|
||||
@@ -124,42 +124,42 @@ func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
|
||||
return subset, nil
|
||||
}
|
||||
|
||||
func (f *Filter) commentIs(comment string) {
|
||||
func (f *IpRouteFilter) commentIs(comment string) {
|
||||
f.queryParams.Set("comment", comment)
|
||||
}
|
||||
|
||||
func (f *Filter) notDeleted() {
|
||||
func (f *IpRouteFilter) notDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *Filter) deleted() {
|
||||
func (f *IpRouteFilter) deleted() {
|
||||
f.queryParams.Set("is_deleted", "true")
|
||||
}
|
||||
|
||||
func (f *Filter) networkIsSubsetOf(superset net.IPNet) {
|
||||
func (f *IpRouteFilter) networkIsSubsetOf(superset net.IPNet) {
|
||||
f.queryParams.Set("network_subset", superset.String())
|
||||
}
|
||||
|
||||
func (f *Filter) networkIsSupersetOf(subset net.IPNet) {
|
||||
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) {
|
||||
f.queryParams.Set("network_superset", subset.String())
|
||||
}
|
||||
|
||||
func (f *Filter) existedAt(existedAt time.Time) {
|
||||
func (f *IpRouteFilter) existedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func (f *Filter) tunnelID(id uuid.UUID) {
|
||||
func (f *IpRouteFilter) tunnelID(id uuid.UUID) {
|
||||
f.queryParams.Set("tunnel_id", id.String())
|
||||
}
|
||||
|
||||
func (f *Filter) vnetID(id uuid.UUID) {
|
||||
func (f *IpRouteFilter) vnetID(id uuid.UUID) {
|
||||
f.queryParams.Set("virtual_network_id", id.String())
|
||||
}
|
||||
|
||||
func (f *Filter) MaxFetchSize(max uint) {
|
||||
func (f *IpRouteFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f Filter) Encode() string {
|
||||
func (f IpRouteFilter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package teamnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
|
||||
|
||||
type Tunnel struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
Connections []Connection `json:"connections"`
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
ColoName string `json:"colo_name"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
IsPendingReconnect bool `json:"is_pending_reconnect"`
|
||||
OriginIP net.IP `json:"origin_ip"`
|
||||
OpenedAt time.Time `json:"opened_at"`
|
||||
}
|
||||
|
||||
type ActiveClient struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Features []string `json:"features"`
|
||||
Version string `json:"version"`
|
||||
Arch string `json:"arch"`
|
||||
RunAt time.Time `json:"run_at"`
|
||||
Connections []Connection `json:"conns"`
|
||||
}
|
||||
|
||||
type newTunnel struct {
|
||||
Name string `json:"name"`
|
||||
TunnelSecret []byte `json:"tunnel_secret"`
|
||||
}
|
||||
|
||||
type CleanupParams struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewCleanupParams() *CleanupParams {
|
||||
return &CleanupParams{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
|
||||
cp.queryParams.Set("client_id", clientID.String())
|
||||
}
|
||||
|
||||
func (cp CleanupParams) encode() string {
|
||||
return cp.queryParams.Encode()
|
||||
}
|
||||
|
||||
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("tunnel name required")
|
||||
}
|
||||
if _, err := uuid.Parse(name); err == nil {
|
||||
return nil, errors.New("you cannot use UUIDs as tunnel names")
|
||||
}
|
||||
body := &newTunnel{
|
||||
Name: name,
|
||||
TunnelSecret: tunnelSecret,
|
||||
}
|
||||
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return unmarshalTunnel(resp.Body)
|
||||
case http.StatusConflict:
|
||||
return nil, ErrTunnelNameConflict
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("create tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return unmarshalTunnel(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("get tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("delete tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = filter.encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListTunnels(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list tunnels", resp)
|
||||
}
|
||||
|
||||
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
|
||||
var tunnels []*Tunnel
|
||||
err := parseResponse(body, &tunnels)
|
||||
return tunnels, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseConnectionsDetails(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list connection details", resp)
|
||||
}
|
||||
|
||||
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
|
||||
var clients []*ActiveClient
|
||||
err := parseResponse(reader, &clients)
|
||||
return clients, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = params.encode()
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("cleanup connections", resp)
|
||||
}
|
||||
|
||||
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
||||
var tunnel Tunnel
|
||||
err := parseResponse(reader, &tunnel)
|
||||
return &tunnel, err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tunnelstore
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
@@ -12,44 +12,44 @@ const (
|
||||
TimeLayout = time.RFC3339
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
type TunnelFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewFilter() *Filter {
|
||||
return &Filter{
|
||||
func NewTunnelFilter() *TunnelFilter {
|
||||
return &TunnelFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filter) ByName(name string) {
|
||||
func (f *TunnelFilter) ByName(name string) {
|
||||
f.queryParams.Set("name", name)
|
||||
}
|
||||
|
||||
func (f *Filter) ByNamePrefix(namePrefix string) {
|
||||
func (f *TunnelFilter) ByNamePrefix(namePrefix string) {
|
||||
f.queryParams.Set("name_prefix", namePrefix)
|
||||
}
|
||||
|
||||
func (f *Filter) ExcludeNameWithPrefix(excludePrefix string) {
|
||||
func (f *TunnelFilter) ExcludeNameWithPrefix(excludePrefix string) {
|
||||
f.queryParams.Set("exclude_prefix", excludePrefix)
|
||||
}
|
||||
|
||||
func (f *Filter) NoDeleted() {
|
||||
func (f *TunnelFilter) NoDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *Filter) ByExistedAt(existedAt time.Time) {
|
||||
func (f *TunnelFilter) ByExistedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(TimeLayout))
|
||||
}
|
||||
|
||||
func (f *Filter) ByTunnelID(tunnelID uuid.UUID) {
|
||||
func (f *TunnelFilter) ByTunnelID(tunnelID uuid.UUID) {
|
||||
f.queryParams.Set("uuid", tunnelID.String())
|
||||
}
|
||||
|
||||
func (f *Filter) MaxFetchSize(max uint) {
|
||||
func (f *TunnelFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f Filter) encode() string {
|
||||
func (f TunnelFilter) encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var loc, _ = time.LoadLocation("UTC")
|
||||
|
||||
func Test_parseListTunnels(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []*Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": []}`},
|
||||
want: []*Tunnel{},
|
||||
},
|
||||
{
|
||||
name: "success is false",
|
||||
args: args{body: `{"success": false, "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors are present",
|
||||
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid response",
|
||||
args: args{body: `abc`},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
|
||||
got, err := parseListTunnels(body)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unmarshalTunnel(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": {"id":"b34cc7ce-925b-46ee-bc23-4cb5c18d8292","created_at":"2021-07-29T13:46:14.090955Z","deleted_at":"2021-07-29T14:07:27.559047Z","name":"qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV","account_id":6946212,"account_tag":"5ab4e9dfbd435d24068829fda0077963","conns_active_at":null,"conns_inactive_at":"2021-07-29T13:47:22.548482Z","tun_type":"cfd_tunnel","metadata":{"qtid":"a6fJROgkXutNruBGaJjD"}}}`},
|
||||
want: &Tunnel{
|
||||
ID: uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292"),
|
||||
Name: "qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV",
|
||||
CreatedAt: time.Date(2021, 07, 29, 13, 46, 14, 90955000, loc),
|
||||
DeletedAt: time.Date(2021, 07, 29, 14, 7, 27, 559047000, loc),
|
||||
Connections: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := unmarshalTunnel(strings.NewReader(tt.args.body))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelOk(t *testing.T) {
|
||||
|
||||
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
|
||||
expected := Tunnel{
|
||||
ID: uuid.Nil,
|
||||
Name: "test",
|
||||
CreatedAt: time.Time{},
|
||||
Connections: []Connection{},
|
||||
}
|
||||
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &expected, actual)
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelErr(t *testing.T) {
|
||||
|
||||
tests := []string{
|
||||
`abc`,
|
||||
`{"success": true, "result": abc}`,
|
||||
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
|
||||
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalConnections(t *testing.T) {
|
||||
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
|
||||
expected := ActiveClient{
|
||||
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
|
||||
Features: []string{"ha-origin"},
|
||||
Version: "2021.2.5",
|
||||
Arch: "darwin_amd64",
|
||||
RunAt: time.Time{},
|
||||
Connections: []Connection{{
|
||||
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
|
||||
ColoName: "LIS",
|
||||
IsPendingReconnect: false,
|
||||
OriginIP: net.ParseIP("148.38.28.2"),
|
||||
OpenedAt: time.Time{},
|
||||
}},
|
||||
}
|
||||
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
||||
}
|
||||
@@ -1,21 +1,59 @@
|
||||
package tunnelstore
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/vnet"
|
||||
)
|
||||
|
||||
func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
|
||||
type NewVirtualNetwork struct {
|
||||
Name string `json:"name"`
|
||||
Comment string `json:"comment"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type VirtualNetwork struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Comment string `json:"comment"`
|
||||
Name string `json:"name"`
|
||||
IsDefault bool `json:"is_default_network"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type UpdateVirtualNetwork struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
IsDefault *bool `json:"is_default_network,omitempty"`
|
||||
}
|
||||
|
||||
func (virtualNetwork VirtualNetwork) TableString() string {
|
||||
deletedColumn := "-"
|
||||
if !virtualNetwork.DeletedAt.IsZero() {
|
||||
deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
virtualNetwork.ID,
|
||||
virtualNetwork.Name,
|
||||
strconv.FormatBool(virtualNetwork.IsDefault),
|
||||
virtualNetwork.Comment,
|
||||
virtualNetwork.CreatedAt.Format(time.RFC3339),
|
||||
deletedColumn,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *RESTClient) CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error) {
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet)
|
||||
if err != nil {
|
||||
return vnet.VirtualNetwork{}, errors.Wrap(err, "REST request failed")
|
||||
return VirtualNetwork{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -23,10 +61,10 @@ func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.
|
||||
return parseVnet(resp.Body)
|
||||
}
|
||||
|
||||
return vnet.VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
|
||||
return VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
|
||||
func (r *RESTClient) ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error) {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
@@ -59,7 +97,7 @@ func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID) error {
|
||||
return r.statusCodeToError("delete virtual network", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
|
||||
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||
resp, err := r.sendRequest("PATCH", endpoint, updates)
|
||||
@@ -76,14 +114,14 @@ func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtu
|
||||
return r.statusCodeToError("update virtual network", resp)
|
||||
}
|
||||
|
||||
func parseListVnets(body io.ReadCloser) ([]*vnet.VirtualNetwork, error) {
|
||||
var vnets []*vnet.VirtualNetwork
|
||||
func parseListVnets(body io.ReadCloser) ([]*VirtualNetwork, error) {
|
||||
var vnets []*VirtualNetwork
|
||||
err := parseResponse(body, &vnets)
|
||||
return vnets, err
|
||||
}
|
||||
|
||||
func parseVnet(body io.ReadCloser) (vnet.VirtualNetwork, error) {
|
||||
var vnet vnet.VirtualNetwork
|
||||
func parseVnet(body io.ReadCloser) (VirtualNetwork, error) {
|
||||
var vnet VirtualNetwork
|
||||
err := parseResponse(body, &vnet)
|
||||
return vnet, err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package vnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
@@ -10,68 +10,68 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
filterId = cli.StringFlag{
|
||||
filterVnetId = cli.StringFlag{
|
||||
Name: "id",
|
||||
Usage: "List virtual networks with the given `ID`",
|
||||
}
|
||||
filterName = cli.StringFlag{
|
||||
filterVnetByName = cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "List virtual networks with the given `NAME`",
|
||||
}
|
||||
filterDefault = cli.BoolFlag{
|
||||
filterDefaultVnet = cli.BoolFlag{
|
||||
Name: "is-default",
|
||||
Usage: "If true, lists the virtual network that is the default one. If false, lists all non-default virtual networks for the account. If absent, all are included in the results regardless of their default status.",
|
||||
}
|
||||
filterDeleted = cli.BoolFlag{
|
||||
filterDeletedVnet = cli.BoolFlag{
|
||||
Name: "show-deleted",
|
||||
Usage: "If false (default), only show non-deleted virtual networks. If true, only show deleted virtual networks.",
|
||||
}
|
||||
FilterFlags = []cli.Flag{
|
||||
&filterId,
|
||||
&filterName,
|
||||
&filterDefault,
|
||||
&filterDeleted,
|
||||
VnetFilterFlags = []cli.Flag{
|
||||
&filterVnetId,
|
||||
&filterVnetByName,
|
||||
&filterDefaultVnet,
|
||||
&filterDeletedVnet,
|
||||
}
|
||||
)
|
||||
|
||||
// Filter which virtual networks get queried.
|
||||
type Filter struct {
|
||||
// VnetFilter which virtual networks get queried.
|
||||
type VnetFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewFilter() *Filter {
|
||||
return &Filter{
|
||||
func NewVnetFilter() *VnetFilter {
|
||||
return &VnetFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filter) ById(vnetId uuid.UUID) {
|
||||
func (f *VnetFilter) ById(vnetId uuid.UUID) {
|
||||
f.queryParams.Set("id", vnetId.String())
|
||||
}
|
||||
|
||||
func (f *Filter) ByName(name string) {
|
||||
func (f *VnetFilter) ByName(name string) {
|
||||
f.queryParams.Set("name", name)
|
||||
}
|
||||
|
||||
func (f *Filter) ByDefaultStatus(isDefault bool) {
|
||||
func (f *VnetFilter) ByDefaultStatus(isDefault bool) {
|
||||
f.queryParams.Set("is_default", strconv.FormatBool(isDefault))
|
||||
}
|
||||
|
||||
func (f *Filter) WithDeleted(isDeleted bool) {
|
||||
func (f *VnetFilter) WithDeleted(isDeleted bool) {
|
||||
f.queryParams.Set("is_deleted", strconv.FormatBool(isDeleted))
|
||||
}
|
||||
|
||||
func (f *Filter) MaxFetchSize(max uint) {
|
||||
func (f *VnetFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f Filter) Encode() string {
|
||||
func (f VnetFilter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
|
||||
// NewFromCLI parses CLI flags to discover which filters should get applied to list virtual networks.
|
||||
func NewFromCLI(c *cli.Context) (*Filter, error) {
|
||||
f := NewFilter()
|
||||
func NewFromCLI(c *cli.Context) (*VnetFilter, error) {
|
||||
f := NewVnetFilter()
|
||||
|
||||
if id := c.String("id"); id != "" {
|
||||
vnetId, err := uuid.Parse(id)
|
||||
@@ -1,4 +1,4 @@
|
||||
package vnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+58
-31
@@ -5,6 +5,15 @@ build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: buster
|
||||
stretch: &stretch
|
||||
build:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
build-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
@@ -14,20 +23,12 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared
|
||||
build-non-fips: # helpful to catch problems with non-fips (only used for releasing non-linux artifacts) before releases
|
||||
# except FIPS (handled in github-fips-release-pkgs) and macos (handled in github-release-macos-amd64)
|
||||
github-release-pkgs:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared
|
||||
build-all-packages: #except osxpkg
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
- rpm
|
||||
@@ -35,14 +36,21 @@ stretch: &stretch
|
||||
# libmsi and libgcab are libraries the wixl binary depends on.
|
||||
- libmsi-dev
|
||||
- libgcab-dev
|
||||
pre-cache:
|
||||
# TODO: https://jira.cfops.it/browse/TUN-4792 Replace this wixl with the official one once msitools supports
|
||||
# environment.
|
||||
- python3-dev
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: &github_release_pkgs_pre_cache
|
||||
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
|
||||
- chmod a+x /usr/local/bin/wixl
|
||||
- pip3 install pygithub
|
||||
post-cache:
|
||||
# build all packages (except macos and FIPS) and move them to /cfsetup/built_artifacts
|
||||
- ./build-packages.sh
|
||||
github-release-pkgs:
|
||||
# release the packages built and moved to /cfsetup/built_artifacts
|
||||
- make github-release-built-pkgs
|
||||
# handle FIPS separately so that we built with gofips compiler
|
||||
github-fips-release-pkgs:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
@@ -58,18 +66,25 @@ stretch: &stretch
|
||||
- libffi-dev
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache:
|
||||
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
|
||||
- chmod a+x /usr/local/bin/wixl
|
||||
- pip3 install pygithub
|
||||
pre-cache: *github_release_pkgs_pre_cache
|
||||
post-cache:
|
||||
# build all packages and move them to /cfsetup/built_artifacts
|
||||
- ./build-packages.sh
|
||||
# release the packages built and moved to /cfsetup/built_artifacts
|
||||
# same logic as above, but for FIPS packages only
|
||||
- ./build-packages-fips.sh
|
||||
- make github-release-built-pkgs
|
||||
build-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_deb_deps
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-deb
|
||||
build-fips-internal-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps: &build_fips_deb_deps
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
@@ -78,16 +93,16 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared-deb
|
||||
build-deb-nightly:
|
||||
- make cloudflared-internal-deb
|
||||
build-fips-internal-deb-nightly:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deb_deps
|
||||
builddeps: *build_fips_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- export NIGHTLY=true
|
||||
- make cloudflared-deb
|
||||
- export FIPS=true
|
||||
- make cloudflared-internal-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deb_deps
|
||||
@@ -98,7 +113,7 @@ stretch: &stretch
|
||||
publish-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- fakeroot
|
||||
- rubygem-fpm
|
||||
@@ -106,7 +121,6 @@ stretch: &stretch
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make publish-deb
|
||||
github-release-macos-amd64:
|
||||
build_dir: *build_dir
|
||||
@@ -122,14 +136,27 @@ stretch: &stretch
|
||||
post-cache:
|
||||
- make github-mac-upload
|
||||
test:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
pre-cache: &test_pre_cache
|
||||
- go get golang.org/x/tools/cmd/goimports
|
||||
- go get github.com/sudarshan-reddy/go-sumtype@v0.0.0-20210827105221-82eca7e5abb1
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export PATH="$HOME/go/bin:$PATH"
|
||||
- ./fmt-check.sh
|
||||
- make test | gotest-to-teamcity
|
||||
test-fips:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- gotest-to-teamcity
|
||||
pre-cache:
|
||||
- go get golang.org/x/tools/cmd/goimports
|
||||
- go get github.com/sudarshan-reddy/go-sumtype@v0.0.0-20210827105221-82eca7e5abb1
|
||||
pre-cache: *test_pre_cache
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package buildinfo
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -11,23 +11,39 @@ type BuildInfo struct {
|
||||
GoOS string `json:"go_os"`
|
||||
GoVersion string `json:"go_version"`
|
||||
GoArch string `json:"go_arch"`
|
||||
BuildType string `json:"build_type"`
|
||||
CloudflaredVersion string `json:"cloudflared_version"`
|
||||
}
|
||||
|
||||
func GetBuildInfo(cloudflaredVersion string) *BuildInfo {
|
||||
func GetBuildInfo(buildType, version string) *BuildInfo {
|
||||
return &BuildInfo{
|
||||
GoOS: runtime.GOOS,
|
||||
GoVersion: runtime.Version(),
|
||||
GoArch: runtime.GOARCH,
|
||||
CloudflaredVersion: cloudflaredVersion,
|
||||
BuildType: buildType,
|
||||
CloudflaredVersion: version,
|
||||
}
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Log(log *zerolog.Logger) {
|
||||
log.Info().Msgf("Version %s", bi.CloudflaredVersion)
|
||||
if bi.BuildType != "" {
|
||||
log.Info().Msgf("Built%s", bi.GetBuildTypeMsg())
|
||||
}
|
||||
log.Info().Msgf("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) OSArch() string {
|
||||
return fmt.Sprintf("%s_%s", bi.GoOS, bi.GoArch)
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Version() string {
|
||||
return bi.CloudflaredVersion
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) GetBuildTypeMsg() string {
|
||||
if bi.BuildType == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(" with %s", bi.BuildType)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows && !darwin && !linux
|
||||
// +build !windows,!darwin,!linux
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package main
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package main
|
||||
|
||||
@@ -31,6 +31,7 @@ const (
|
||||
var (
|
||||
Version = "DEV"
|
||||
BuildTime = "unknown"
|
||||
BuildType = ""
|
||||
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
|
||||
ignoredErrors = []string{
|
||||
"connection reset by peer",
|
||||
@@ -46,9 +47,10 @@ var (
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
metrics.RegisterBuildInfo(BuildTime, Version)
|
||||
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
||||
raven.SetRelease(Version)
|
||||
maxprocs.Set()
|
||||
bInfo := cliutil.GetBuildInfo(BuildType, Version)
|
||||
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate gracefully.
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
@@ -71,7 +73,7 @@ func main() {
|
||||
Terms (https://www.cloudflare.com/terms/) and Privacy Policy (https://www.cloudflare.com/privacypolicy/).`,
|
||||
time.Now().Year(),
|
||||
)
|
||||
app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime)
|
||||
app.Version = fmt.Sprintf("%s (built %s%s)", Version, BuildTime, bInfo.GetBuildTypeMsg())
|
||||
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
|
||||
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
|
||||
and configure access control.
|
||||
@@ -81,7 +83,7 @@ func main() {
|
||||
app.Action = action(graceShutdownC)
|
||||
app.Commands = commands(cli.ShowVersion)
|
||||
|
||||
tunnel.Init(Version, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
tunnel.Init(bInfo, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(graceShutdownC)
|
||||
updater.Init(Version)
|
||||
runApp(app, graceShutdownC)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
|
||||
@@ -35,7 +35,6 @@ import (
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -86,7 +85,7 @@ const (
|
||||
|
||||
var (
|
||||
graceShutdownC chan struct{}
|
||||
version string
|
||||
buildInfo *cliutil.BuildInfo
|
||||
|
||||
routeFailMsg = fmt.Sprintf("failed to provision routing, please create it manually via Cloudflare dashboard or UI; "+
|
||||
"most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+
|
||||
@@ -175,8 +174,8 @@ func TunnelCommand(c *cli.Context) error {
|
||||
return runClassicTunnel(sc)
|
||||
}
|
||||
|
||||
func Init(ver string, gracefulShutdown chan struct{}) {
|
||||
version, graceShutdownC = ver, gracefulShutdown
|
||||
func Init(info *cliutil.BuildInfo, gracefulShutdown chan struct{}) {
|
||||
buildInfo, graceShutdownC = info, gracefulShutdown
|
||||
}
|
||||
|
||||
// runAdhocNamedTunnel create, route and run a named tunnel in one command
|
||||
@@ -209,22 +208,22 @@ func runAdhocNamedTunnel(sc *subcommandContext, name, credentialsOutputPath stri
|
||||
|
||||
// runClassicTunnel creates a "classic" non-named tunnel
|
||||
func runClassicTunnel(sc *subcommandContext) error {
|
||||
return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled)
|
||||
return StartServer(sc.c, buildInfo, nil, sc.log, sc.isUIEnabled)
|
||||
}
|
||||
|
||||
func routeFromFlag(c *cli.Context) (route tunnelstore.Route, ok bool) {
|
||||
func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
|
||||
if hostname := c.String("hostname"); hostname != "" {
|
||||
if lbPool := c.String("lb-pool"); lbPool != "" {
|
||||
return tunnelstore.NewLBRoute(hostname, lbPool), true
|
||||
return cfapi.NewLBRoute(hostname, lbPool), true
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
|
||||
return cfapi.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func StartServer(
|
||||
c *cli.Context,
|
||||
version string,
|
||||
info *cliutil.BuildInfo,
|
||||
namedTunnel *connection.NamedTunnelConfig,
|
||||
log *zerolog.Logger,
|
||||
isUIEnabled bool,
|
||||
@@ -271,8 +270,7 @@ func StartServer(
|
||||
defer trace.Stop()
|
||||
}
|
||||
|
||||
buildInfo := buildinfo.GetBuildInfo(version)
|
||||
buildInfo.Log(log)
|
||||
info.Log(log)
|
||||
logClientOptions(c, log)
|
||||
|
||||
// this context drives the server, when it's cancelled tunnel and all other components (origins, dns, etc...) should stop
|
||||
@@ -336,7 +334,7 @@ func StartServer(
|
||||
observer.SendURL(quickTunnelURL)
|
||||
}
|
||||
|
||||
tunnelConfig, ingressRules, err := prepareTunnelConfig(c, buildInfo, version, log, logTransport, observer, namedTunnel)
|
||||
tunnelConfig, ingressRules, err := prepareTunnelConfig(c, info, log, logTransport, observer, namedTunnel)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Couldn't start tunnel")
|
||||
return err
|
||||
@@ -377,7 +375,7 @@ func StartServer(
|
||||
|
||||
if isUIEnabled {
|
||||
tunnelUI := ui.NewUIModel(
|
||||
version,
|
||||
info.Version(),
|
||||
hostname,
|
||||
metricsListener.Addr().String(),
|
||||
&ingressRules,
|
||||
|
||||
@@ -16,7 +16,8 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
@@ -148,8 +149,7 @@ func getOriginCert(originCertPath string, log *zerolog.Logger) ([]byte, error) {
|
||||
|
||||
func prepareTunnelConfig(
|
||||
c *cli.Context,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
version string,
|
||||
info *cliutil.BuildInfo,
|
||||
log, logTransport *zerolog.Logger,
|
||||
observer *connection.Observer,
|
||||
namedTunnel *connection.NamedTunnelConfig,
|
||||
@@ -193,8 +193,8 @@ func prepareTunnelConfig(
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
ClientID: clientUUID[:],
|
||||
Features: dedup(features),
|
||||
Version: version,
|
||||
Arch: buildInfo.OSArch(),
|
||||
Version: info.Version(),
|
||||
Arch: info.OSArch(),
|
||||
}
|
||||
ingressRules, err = ingress.ParseIngress(cfg)
|
||||
if err != nil && err != ingress.ErrNoIngressRules {
|
||||
@@ -281,7 +281,7 @@ func prepareTunnelConfig(
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
ConnectionConfig: connectionConfig,
|
||||
OSArch: buildInfo.OSArch(),
|
||||
OSArch: info.OSArch(),
|
||||
ClientID: clientID,
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
Region: c.String("region"),
|
||||
@@ -293,7 +293,7 @@ func prepareTunnelConfig(
|
||||
Log: log,
|
||||
LogTransport: logTransport,
|
||||
Observer: observer,
|
||||
ReportedVersion: version,
|
||||
ReportedVersion: info.Version(),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
Retries: uint(c.Int("retries")),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// TODO: Remove the above build tag and include this test when we start compiling with Golang 1.10.0+
|
||||
|
||||
package tunnel
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Connectors []*tunnelstore.ActiveClient `json:"conns"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Connectors []*cfapi.ActiveClient `json:"conns"`
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
version,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package tunnel
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/certutil"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
type errInvalidJSONCredential struct {
|
||||
@@ -37,7 +37,7 @@ type subcommandContext struct {
|
||||
fs fileSystem
|
||||
|
||||
// These fields should be accessed using their respective Getter
|
||||
tunnelstoreClient tunnelstore.Client
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ type userCredential struct {
|
||||
certPath string
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) client() (tunnelstore.Client, error) {
|
||||
func (sc *subcommandContext) client() (cfapi.Client, error) {
|
||||
if sc.tunnelstoreClient != nil {
|
||||
return sc.tunnelstoreClient, nil
|
||||
}
|
||||
@@ -76,8 +76,8 @@ func (sc *subcommandContext) client() (tunnelstore.Client, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAgent := fmt.Sprintf("cloudflared/%s", version)
|
||||
client, err := tunnelstore.NewRESTClient(
|
||||
userAgent := fmt.Sprintf("cloudflared/%s", buildInfo.Version())
|
||||
client, err := cfapi.NewRESTClient(
|
||||
sc.c.String("api-url"),
|
||||
credential.cert.AccountID,
|
||||
credential.cert.ZoneID,
|
||||
@@ -149,7 +149,7 @@ func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (conne
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*tunnelstore.Tunnel, error) {
|
||||
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*cfapi.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't create client to talk to Cloudflare Tunnel backend")
|
||||
@@ -224,7 +224,7 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
|
||||
return tunnel, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) list(filter *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) {
|
||||
func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -251,7 +251,7 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
||||
}
|
||||
if forceFlagSet {
|
||||
if err := client.CleanupConnections(tunnel.ID, tunnelstore.NewCleanupParams()); err != nil {
|
||||
if err := client.CleanupConnections(tunnel.ID, cfapi.NewCleanupParams()); err != nil {
|
||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
||||
}
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
version,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
@@ -311,7 +311,7 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||
params := tunnelstore.NewCleanupParams()
|
||||
params := cfapi.NewCleanupParams()
|
||||
extraLog := ""
|
||||
if connector := sc.c.String("connector-id"); connector != "" {
|
||||
connectorID, err := uuid.Parse(connector)
|
||||
@@ -335,7 +335,7 @@ func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tunnelstore.RouteResult, error) {
|
||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -345,8 +345,8 @@ func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tun
|
||||
}
|
||||
|
||||
// Query Tunnelstore to find the active tunnel with the given name.
|
||||
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
|
||||
filter := tunnelstore.NewFilter()
|
||||
func (sc *subcommandContext) tunnelActive(name string) (*cfapi.Tunnel, bool, error) {
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
tunnels, err := sc.list(filter)
|
||||
@@ -391,7 +391,7 @@ func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
||||
uuids, names := splitUuids(inputs)
|
||||
|
||||
for _, name := range names {
|
||||
filter := tunnelstore.NewFilter()
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ package tunnel
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
const noClientMsg = "error while creating backend client"
|
||||
|
||||
func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error) {
|
||||
func (sc *subcommandContext) listRoutes(filter *cfapi.IpRouteFilter) ([]*cfapi.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, noClientMsg)
|
||||
@@ -16,15 +16,15 @@ func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.Deta
|
||||
return client.ListRoutes(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) addRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
|
||||
func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return teamnet.Route{}, errors.Wrap(err, noClientMsg)
|
||||
return cfapi.Route{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.AddRoute(newRoute)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error {
|
||||
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
@@ -32,10 +32,10 @@ func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error
|
||||
return client.DeleteRoute(params)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) getRouteByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
|
||||
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return teamnet.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
||||
return cfapi.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.GetByIP(params)
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
type mockFileSystem struct {
|
||||
@@ -36,7 +36,7 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
log *zerolog.Logger
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
tunnelstoreClient tunnelstore.Client
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
type args struct {
|
||||
@@ -187,13 +187,13 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
}
|
||||
|
||||
type deleteMockTunnelStore struct {
|
||||
tunnelstore.Client
|
||||
cfapi.Client
|
||||
mockTunnels map[uuid.UUID]mockTunnelBehaviour
|
||||
deletedTunnelIDs []uuid.UUID
|
||||
}
|
||||
|
||||
type mockTunnelBehaviour struct {
|
||||
tunnel tunnelstore.Tunnel
|
||||
tunnel cfapi.Tunnel
|
||||
deleteErr error
|
||||
cleanupErr error
|
||||
}
|
||||
@@ -209,7 +209,7 @@ func newDeleteMockTunnelStore(tunnels ...mockTunnelBehaviour) *deleteMockTunnelS
|
||||
}
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
|
||||
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
@@ -233,7 +233,7 @@ func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *tunnelstore.CleanupParams) error {
|
||||
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *cfapi.CleanupParams) error {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
@@ -284,10 +284,10 @@ func Test_subcommandContext_Delete(t *testing.T) {
|
||||
}(),
|
||||
tunnelstoreClient: newDeleteMockTunnelStore(
|
||||
mockTunnelBehaviour{
|
||||
tunnel: tunnelstore.Tunnel{ID: tunnelID1},
|
||||
tunnel: cfapi.Tunnel{ID: tunnelID1},
|
||||
},
|
||||
mockTunnelBehaviour{
|
||||
tunnel: tunnelstore.Tunnel{ID: tunnelID2},
|
||||
tunnel: cfapi.Tunnel{ID: tunnelID2},
|
||||
},
|
||||
),
|
||||
},
|
||||
|
||||
@@ -4,18 +4,18 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/vnet"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
func (sc *subcommandContext) addVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
|
||||
func (sc *subcommandContext) addVirtualNetwork(newVnet cfapi.NewVirtualNetwork) (cfapi.VirtualNetwork, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return vnet.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
|
||||
return cfapi.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.CreateVirtualNetwork(newVnet)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) listVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
|
||||
func (sc *subcommandContext) listVirtualNetworks(filter *cfapi.VnetFilter) ([]*cfapi.VirtualNetwork, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, noClientMsg)
|
||||
@@ -31,7 +31,7 @@ func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID) error {
|
||||
return client.DeleteVirtualNetwork(vnetId)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
|
||||
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates cfapi.UpdateVirtualNetwork) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"golang.org/x/net/idna"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -64,8 +64,8 @@ var (
|
||||
Name: "when",
|
||||
Aliases: []string{"w"},
|
||||
Usage: "List tunnels that are active at the given `TIME` in RFC3339 format",
|
||||
Layout: tunnelstore.TimeLayout,
|
||||
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(tunnelstore.TimeLayout)),
|
||||
Layout: cfapi.TimeLayout,
|
||||
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(cfapi.TimeLayout)),
|
||||
}
|
||||
listIDFlag = &cli.StringFlag{
|
||||
Name: "id",
|
||||
@@ -260,7 +260,7 @@ func listCommand(c *cli.Context) error {
|
||||
warningChecker := updater.StartWarningCheck(c)
|
||||
defer warningChecker.LogWarningIfAny(sc.log)
|
||||
|
||||
filter := tunnelstore.NewFilter()
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
if !c.Bool("show-deleted") {
|
||||
filter.NoDeleted()
|
||||
}
|
||||
@@ -335,7 +335,7 @@ func listCommand(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
|
||||
func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected bool) {
|
||||
writer := tabWriter()
|
||||
defer writer.Flush()
|
||||
|
||||
@@ -357,7 +357,7 @@ func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconn
|
||||
}
|
||||
}
|
||||
|
||||
func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnected bool) string {
|
||||
func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string {
|
||||
|
||||
// Count connections per colo
|
||||
numConnsPerColo := make(map[string]uint, len(connections))
|
||||
@@ -477,8 +477,8 @@ func tunnelInfo(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
|
||||
filter := tunnelstore.NewFilter()
|
||||
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.ByTunnelID(tunnelID)
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
@@ -711,7 +711,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
|
||||
{
|
||||
Name: "dns",
|
||||
Action: cliutil.ConfiguredAction(routeDnsCommand),
|
||||
Usage: "Route a hostname by creating a DNS CNAME record to a tunnel",
|
||||
Usage: "HostnameRoute a hostname by creating a DNS CNAME record to a tunnel",
|
||||
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME]",
|
||||
Description: `Creates a DNS CNAME record hostname that points to the tunnel.`,
|
||||
Flags: []cli.Flag{overwriteDNSFlag},
|
||||
@@ -728,7 +728,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route, error) {
|
||||
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (cfapi.HostnameRoute, error) {
|
||||
const (
|
||||
userHostnameIndex = 1
|
||||
expectedNArgs = 2
|
||||
@@ -742,10 +742,10 @@ func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route,
|
||||
} else if !validateHostname(userHostname, true) {
|
||||
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(userHostname, overwriteExisting), nil
|
||||
return cfapi.NewDNSRoute(userHostname, overwriteExisting), nil
|
||||
}
|
||||
|
||||
func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
func lbRouteFromArg(c *cli.Context) (cfapi.HostnameRoute, error) {
|
||||
const (
|
||||
lbNameIndex = 1
|
||||
lbPoolIndex = 2
|
||||
@@ -768,7 +768,7 @@ func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
return nil, errors.Errorf("%s is not a valid pool name", lbPool)
|
||||
}
|
||||
|
||||
return tunnelstore.NewLBRoute(lbName, lbPool), nil
|
||||
return cfapi.NewLBRoute(lbName, lbPool), nil
|
||||
}
|
||||
|
||||
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
|
||||
@@ -815,7 +815,7 @@ func routeCommand(c *cli.Context, routeType string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var route tunnelstore.Route
|
||||
var route cfapi.HostnameRoute
|
||||
switch routeType {
|
||||
case "dns":
|
||||
route, err = dnsRouteFromArg(c, c.Bool(overwriteDNSFlagName))
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
func Test_fmtConnections(t *testing.T) {
|
||||
type args struct {
|
||||
connections []tunnelstore.Connection
|
||||
connections []cfapi.Connection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -23,14 +23,14 @@ func Test_fmtConnections(t *testing.T) {
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{},
|
||||
connections: []cfapi.Connection{},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "trivial",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
connections: []cfapi.Connection{
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
@@ -42,7 +42,7 @@ func Test_fmtConnections(t *testing.T) {
|
||||
{
|
||||
name: "with a pending reconnect",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
connections: []cfapi.Connection{
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
@@ -55,7 +55,7 @@ func Test_fmtConnections(t *testing.T) {
|
||||
{
|
||||
name: "many colos",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
connections: []cfapi.Connection{
|
||||
{
|
||||
ColoName: "YRV",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
|
||||
@@ -8,12 +8,11 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -92,7 +91,7 @@ to tell which virtual network whose routing table you want to use.`,
|
||||
|
||||
func showRoutesFlags() []cli.Flag {
|
||||
flags := make([]cli.Flag, 0)
|
||||
flags = append(flags, teamnet.FilterFlags...)
|
||||
flags = append(flags, cfapi.IpRouteFilterFlags...)
|
||||
flags = append(flags, outputFormatFlag)
|
||||
return flags
|
||||
}
|
||||
@@ -103,7 +102,7 @@ func showRoutesCommand(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
filter, err := teamnet.NewFromCLI(c)
|
||||
filter, err := cfapi.NewIpRouteFilterFromCLI(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid config for routing filters")
|
||||
}
|
||||
@@ -168,7 +167,7 @@ func addRouteCommand(c *cli.Context) error {
|
||||
vnetId = &id
|
||||
}
|
||||
|
||||
_, err = sc.addRoute(teamnet.NewRoute{
|
||||
_, err = sc.addRoute(cfapi.NewRoute{
|
||||
Comment: comment,
|
||||
Network: *network,
|
||||
TunnelID: tunnelID,
|
||||
@@ -199,7 +198,7 @@ func deleteRouteCommand(c *cli.Context) error {
|
||||
return errors.New("Invalid network CIDR")
|
||||
}
|
||||
|
||||
params := teamnet.DeleteRouteParams{
|
||||
params := cfapi.DeleteRouteParams{
|
||||
Network: *network,
|
||||
}
|
||||
|
||||
@@ -233,7 +232,7 @@ func getRouteByIPCommand(c *cli.Context) error {
|
||||
return fmt.Errorf("Invalid IP %s", ipInput)
|
||||
}
|
||||
|
||||
params := teamnet.GetRouteByIpParams{
|
||||
params := cfapi.GetRouteByIpParams{
|
||||
Ip: ip,
|
||||
}
|
||||
|
||||
@@ -252,12 +251,12 @@ func getRouteByIPCommand(c *cli.Context) error {
|
||||
if route.IsZero() {
|
||||
fmt.Printf("No route matches the IP %s\n", ip)
|
||||
} else {
|
||||
formatAndPrintRouteList([]*teamnet.DetailedRoute{&route})
|
||||
formatAndPrintRouteList([]*cfapi.DetailedRoute{&route})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAndPrintRouteList(routes []*teamnet.DetailedRoute) {
|
||||
func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/vnet"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -102,7 +102,7 @@ default or update an existing one to become the default.`,
|
||||
|
||||
func listVirtualNetworksFlags() []cli.Flag {
|
||||
flags := make([]cli.Flag, 0)
|
||||
flags = append(flags, vnet.FilterFlags...)
|
||||
flags = append(flags, cfapi.VnetFilterFlags...)
|
||||
flags = append(flags, outputFormatFlag)
|
||||
return flags
|
||||
}
|
||||
@@ -128,7 +128,7 @@ func addVirtualNetworkCommand(c *cli.Context) error {
|
||||
comment = args.Get(1)
|
||||
}
|
||||
|
||||
newVnet := vnet.NewVirtualNetwork{
|
||||
newVnet := cfapi.NewVirtualNetwork{
|
||||
Name: name,
|
||||
Comment: comment,
|
||||
IsDefault: c.Bool(makeDefaultFlag.Name),
|
||||
@@ -160,7 +160,7 @@ func listVirtualNetworksCommand(c *cli.Context) error {
|
||||
warningChecker := updater.StartWarningCheck(c)
|
||||
defer warningChecker.LogWarningIfAny(sc.log)
|
||||
|
||||
filter, err := vnet.NewFromCLI(c)
|
||||
filter, err := cfapi.NewFromCLI(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid flags for filtering virtual networks")
|
||||
}
|
||||
@@ -220,7 +220,7 @@ func updateVirtualNetworkCommand(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
updates := vnet.UpdateVirtualNetwork{}
|
||||
updates := cfapi.UpdateVirtualNetwork{}
|
||||
|
||||
if c.IsSet(newNameFlag.Name) {
|
||||
newName := c.String(newNameFlag.Name)
|
||||
@@ -248,7 +248,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
filter := vnet.NewFilter()
|
||||
filter := cfapi.NewVnetFilter()
|
||||
filter.WithDeleted(false)
|
||||
filter.ByName(input)
|
||||
|
||||
@@ -264,7 +264,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
|
||||
return vnets[0].ID, nil
|
||||
}
|
||||
|
||||
func formatAndPrintVnetsList(vnets []*vnet.VirtualNetwork) {
|
||||
func formatAndPrintVnetsList(vnets []*cfapi.VirtualNetwork) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package updater
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
+48
-12
@@ -29,8 +29,7 @@ const (
|
||||
// HTTPMethodKey is used to get or set http method in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPMethodKey = "HttpMethod"
|
||||
// HTTPHostKey is used to get or set http Method in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPHostKey = "HttpHost"
|
||||
MaxDatagramFrameSize = 1220
|
||||
HTTPHostKey = "HttpHost"
|
||||
)
|
||||
|
||||
// QUICConnection represents the type that facilitates Proxying via QUIC streams.
|
||||
@@ -168,6 +167,7 @@ func (q *QUICConnection) handleRPCStream(rpcStream *quicpogs.RPCServerStream) er
|
||||
return rpcStream.Serve(q, q.logger)
|
||||
}
|
||||
|
||||
// RegisterUdpSession is the RPC method invoked by edge to register and run a session
|
||||
func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) error {
|
||||
// Each session is a series of datagram from an eyeball to a dstIP:dstPort.
|
||||
// (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket.
|
||||
@@ -178,22 +178,58 @@ func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.
|
||||
}
|
||||
session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy)
|
||||
if err != nil {
|
||||
q.logger.Err(err).Msgf("Failed to register udp session %s", sessionID)
|
||||
q.logger.Err(err).Str("sessionID", sessionID.String()).Msgf("Failed to register udp session")
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
defer q.sessionManager.UnregisterSession(q.session.Context(), sessionID)
|
||||
if err := session.Serve(q.session.Context(), closeAfterIdleHint); err != nil {
|
||||
q.logger.Debug().Err(err).Str("sessionID", sessionID.String()).Msg("session terminated")
|
||||
}
|
||||
}()
|
||||
|
||||
go q.serveUDPSession(session, closeAfterIdleHint)
|
||||
|
||||
q.logger.Debug().Msgf("Registered session %v, %v, %v", sessionID, dstIP, dstPort)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *QUICConnection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error {
|
||||
q.sessionManager.UnregisterSession(ctx, sessionID)
|
||||
return nil
|
||||
func (q *QUICConnection) serveUDPSession(session *datagramsession.Session, closeAfterIdleHint time.Duration) {
|
||||
ctx := q.session.Context()
|
||||
closedByRemote, err := session.Serve(ctx, closeAfterIdleHint)
|
||||
// If session is terminated by remote, then we know it has been unregistered from session manager and edge
|
||||
if !closedByRemote {
|
||||
if err != nil {
|
||||
q.closeUDPSession(ctx, session.ID, err.Error())
|
||||
} else {
|
||||
q.closeUDPSession(ctx, session.ID, "terminated without error")
|
||||
}
|
||||
}
|
||||
q.logger.Debug().Err(err).Str("sessionID", session.ID.String()).Msg("Session terminated")
|
||||
}
|
||||
|
||||
// closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge
|
||||
func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUID, message string) {
|
||||
q.sessionManager.UnregisterSession(ctx, sessionID, message, false)
|
||||
stream, err := q.session.OpenStream()
|
||||
if err != nil {
|
||||
// Log this at debug because this is not an error if session was closed due to lost connection
|
||||
// with edge
|
||||
q.logger.Debug().Err(err).Str("sessionID", sessionID.String()).
|
||||
Msgf("Failed to open quic stream to unregister udp session with edge")
|
||||
return
|
||||
}
|
||||
rpcClientStream, err := quicpogs.NewRPCClientStream(ctx, stream, q.logger)
|
||||
if err != nil {
|
||||
// Log this at debug because this is not an error if session was closed due to lost connection
|
||||
// with edge
|
||||
q.logger.Err(err).Str("sessionID", sessionID.String()).
|
||||
Msgf("Failed to open rpc stream to unregister udp session with edge")
|
||||
return
|
||||
}
|
||||
if err := rpcClientStream.UnregisterUdpSession(ctx, sessionID, message); err != nil {
|
||||
q.logger.Err(err).Str("sessionID", sessionID.String()).
|
||||
Msgf("Failed to unregister udp session with edge")
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterUdpSession is the RPC method invoked by edge to unregister and terminate a sesssion
|
||||
func (q *QUICConnection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error {
|
||||
return q.sessionManager.UnregisterSession(ctx, sessionID, message, true)
|
||||
}
|
||||
|
||||
// streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to
|
||||
|
||||
+170
-35
@@ -17,29 +17,32 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/google/uuid"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/datagramsession"
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
var (
|
||||
testTLSServerConfig = generateTLSConfig()
|
||||
testQUICConfig = &quic.Config{
|
||||
KeepAlive: true,
|
||||
EnableDatagrams: true,
|
||||
}
|
||||
)
|
||||
|
||||
// TestQUICServer tests if a quic server accepts and responds to a quic client with the acceptance protocol.
|
||||
// It also serves as a demonstration for communication with the QUIC connection started by a cloudflared.
|
||||
func TestQUICServer(t *testing.T) {
|
||||
quicConfig := &quic.Config{
|
||||
KeepAlive: true,
|
||||
EnableDatagrams: true,
|
||||
}
|
||||
|
||||
// Setup test.
|
||||
log := zerolog.New(os.Stdout)
|
||||
|
||||
// Start a UDP Listener for QUIC.
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
@@ -47,18 +50,6 @@ func TestQUICServer(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
|
||||
// Create a simple tls config.
|
||||
tlsConfig := generateTLSConfig()
|
||||
|
||||
// Create a client config
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
|
||||
// Start a mock httpProxy
|
||||
originProxy := &mockOriginProxyWithRequest{}
|
||||
|
||||
// This is simply a sample websocket frame message.
|
||||
wsBuf := &bytes.Buffer{}
|
||||
wsutil.WriteClientText(wsBuf, []byte("Hello"))
|
||||
@@ -158,25 +149,13 @@ func TestQUICServer(t *testing.T) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
quicServer(
|
||||
t, udpListener, tlsConfig, quicConfig,
|
||||
t, udpListener, testTLSServerConfig, testQUICConfig,
|
||||
test.dest, test.connectionType, test.metadata, test.message, test.expectedResponse,
|
||||
)
|
||||
}()
|
||||
|
||||
controlStream := fakeControlStream{}
|
||||
|
||||
qC, err := NewQUICConnection(
|
||||
ctx,
|
||||
quicConfig,
|
||||
udpListener.LocalAddr(),
|
||||
tlsClientConfig,
|
||||
originProxy,
|
||||
&tunnelpogs.ConnectionOptions{},
|
||||
controlStream,
|
||||
NewObserver(&log, &log, false),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
go qC.Serve(ctx)
|
||||
qc := testQUICConnection(ctx, udpListener.LocalAddr(), t)
|
||||
go qc.Serve(ctx)
|
||||
|
||||
wg.Wait()
|
||||
cancel()
|
||||
@@ -531,3 +510,159 @@ func (moc *mockOriginProxyWithRequest) ProxyTCP(ctx context.Context, rwa ReadWri
|
||||
io.Copy(rwa, rwa)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestServeUDPSession(t *testing.T) {
|
||||
// Start a UDP Listener for QUIC.
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
|
||||
require.NoError(t, err)
|
||||
defer udpListener.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Establish QUIC connection with edge
|
||||
edgeQUICSessionChan := make(chan quic.Session)
|
||||
go func() {
|
||||
earlyListener, err := quic.Listen(udpListener, testTLSServerConfig, testQUICConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
edgeQUICSession, err := earlyListener.Accept(ctx)
|
||||
require.NoError(t, err)
|
||||
edgeQUICSessionChan <- edgeQUICSession
|
||||
}()
|
||||
|
||||
qc := testQUICConnection(ctx, udpListener.LocalAddr(), t)
|
||||
go qc.Serve(ctx)
|
||||
|
||||
edgeQUICSession := <-edgeQUICSessionChan
|
||||
serveSession(ctx, qc, edgeQUICSession, closedByOrigin, io.EOF.Error(), t)
|
||||
serveSession(ctx, qc, edgeQUICSession, closedByTimeout, datagramsession.SessionIdleErr(time.Millisecond*50).Error(), t)
|
||||
serveSession(ctx, qc, edgeQUICSession, closedByRemote, "eyeball closed connection", t)
|
||||
cancel()
|
||||
}
|
||||
|
||||
func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.Session, closeType closeReason, expectedReason string, t *testing.T) {
|
||||
var (
|
||||
payload = []byte(t.Name())
|
||||
)
|
||||
sessionID := uuid.New()
|
||||
cfdConn, originConn := net.Pipe()
|
||||
// Registers and run a new session
|
||||
session, err := qc.sessionManager.RegisterSession(ctx, sessionID, cfdConn)
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionDone := make(chan struct{})
|
||||
go func() {
|
||||
qc.serveUDPSession(session, time.Millisecond*50)
|
||||
close(sessionDone)
|
||||
}()
|
||||
|
||||
// Send a message to the quic session on edge side, it should be deumx to this datagram session
|
||||
muxedPayload, err := quicpogs.SuffixSessionID(sessionID, payload)
|
||||
require.NoError(t, err)
|
||||
err = edgeQUICSession.SendMessage(muxedPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
readBuffer := make([]byte, len(payload)+1)
|
||||
n, err := originConn.Read(readBuffer)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(payload), n)
|
||||
require.True(t, bytes.Equal(payload, readBuffer[:n]))
|
||||
|
||||
// Close connection to terminate session
|
||||
switch closeType {
|
||||
case closedByOrigin:
|
||||
originConn.Close()
|
||||
case closedByRemote:
|
||||
err = qc.UnregisterUdpSession(ctx, sessionID, expectedReason)
|
||||
require.NoError(t, err)
|
||||
case closedByTimeout:
|
||||
}
|
||||
|
||||
if closeType != closedByRemote {
|
||||
// Session was not closed by remote, so closeUDPSession should be invoked to unregister from remote
|
||||
unregisterFromEdgeChan := make(chan struct{})
|
||||
rpcServer := &mockSessionRPCServer{
|
||||
sessionID: sessionID,
|
||||
unregisterReason: expectedReason,
|
||||
calledUnregisterChan: unregisterFromEdgeChan,
|
||||
}
|
||||
go runMockSessionRPCServer(ctx, edgeQUICSession, rpcServer, t)
|
||||
|
||||
<-unregisterFromEdgeChan
|
||||
}
|
||||
|
||||
<-sessionDone
|
||||
}
|
||||
|
||||
type closeReason uint8
|
||||
|
||||
const (
|
||||
closedByOrigin closeReason = iota
|
||||
closedByRemote
|
||||
closedByTimeout
|
||||
)
|
||||
|
||||
func runMockSessionRPCServer(ctx context.Context, session quic.Session, rpcServer *mockSessionRPCServer, t *testing.T) {
|
||||
stream, err := session.AcceptStream(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
if stream.StreamID() == 0 {
|
||||
// Skip the first stream, it's the control stream of the QUIC connection
|
||||
stream, err = session.AcceptStream(ctx)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
protocol, err := quicpogs.DetermineProtocol(stream)
|
||||
assert.NoError(t, err)
|
||||
rpcServerStream, err := quicpogs.NewRPCServerStream(stream, protocol)
|
||||
assert.NoError(t, err)
|
||||
|
||||
log := zerolog.New(os.Stdout)
|
||||
err = rpcServerStream.Serve(rpcServer, &log)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
type mockSessionRPCServer struct {
|
||||
sessionID uuid.UUID
|
||||
unregisterReason string
|
||||
calledUnregisterChan chan struct{}
|
||||
}
|
||||
|
||||
func (s mockSessionRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error {
|
||||
return fmt.Errorf("mockSessionRPCServer doesn't implement RegisterUdpSession")
|
||||
}
|
||||
|
||||
func (s mockSessionRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, reason string) error {
|
||||
if s.sessionID != sessionID {
|
||||
return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID)
|
||||
}
|
||||
if s.unregisterReason != reason {
|
||||
return fmt.Errorf("expect unregister reason %s, got %s", s.unregisterReason, reason)
|
||||
}
|
||||
close(s.calledUnregisterChan)
|
||||
fmt.Println("unregister from edge")
|
||||
return nil
|
||||
}
|
||||
|
||||
func testQUICConnection(ctx context.Context, udpListenerAddr net.Addr, t *testing.T) *QUICConnection {
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
// Start a mock httpProxy
|
||||
originProxy := &mockOriginProxyWithRequest{}
|
||||
log := zerolog.New(os.Stdout)
|
||||
qc, err := NewQUICConnection(
|
||||
ctx,
|
||||
testQUICConfig,
|
||||
udpListenerAddr,
|
||||
tlsClientConfig,
|
||||
originProxy,
|
||||
&tunnelpogs.ConnectionOptions{},
|
||||
fakeControlStream{},
|
||||
NewObserver(&log, &log, false),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return qc
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package datagramsession
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -24,6 +25,22 @@ func newRegisterSessionEvent(sessionID uuid.UUID, originProxy io.ReadWriteCloser
|
||||
// unregisterSessionEvent is an event to stop tracking and terminate the session.
|
||||
type unregisterSessionEvent struct {
|
||||
sessionID uuid.UUID
|
||||
err *errClosedSession
|
||||
}
|
||||
|
||||
// ClosedSessionError represent a condition that closes the session other than I/O
|
||||
// I/O error is not included, because the side that closes the session is ambiguous.
|
||||
type errClosedSession struct {
|
||||
message string
|
||||
byRemote bool
|
||||
}
|
||||
|
||||
func (sc *errClosedSession) Error() string {
|
||||
if sc.byRemote {
|
||||
return fmt.Sprintf("session closed by remote due to %s", sc.message)
|
||||
} else {
|
||||
return fmt.Sprintf("session closed by local due to %s", sc.message)
|
||||
}
|
||||
}
|
||||
|
||||
// newDatagram is an event when transport receives new datagram
|
||||
|
||||
@@ -20,7 +20,7 @@ type Manager interface {
|
||||
// RegisterSession starts tracking a session. Caller is responsible for starting the session
|
||||
RegisterSession(ctx context.Context, sessionID uuid.UUID, dstConn io.ReadWriteCloser) (*Session, error)
|
||||
// UnregisterSession stops tracking the session and terminates it
|
||||
UnregisterSession(ctx context.Context, sessionID uuid.UUID) error
|
||||
UnregisterSession(ctx context.Context, sessionID uuid.UUID, message string, byRemote bool) error
|
||||
}
|
||||
|
||||
type manager struct {
|
||||
@@ -50,7 +50,6 @@ func (m *manager) Serve(ctx context.Context) error {
|
||||
for {
|
||||
sessionID, payload, err := m.transport.ReceiveFrom()
|
||||
if err != nil {
|
||||
m.log.Err(err).Msg("Failed to receive datagram from transport, closing session manager")
|
||||
return err
|
||||
}
|
||||
datagram := &newDatagram{
|
||||
@@ -96,13 +95,19 @@ func (m *manager) RegisterSession(ctx context.Context, sessionID uuid.UUID, orig
|
||||
}
|
||||
|
||||
func (m *manager) registerSession(ctx context.Context, registration *registerSessionEvent) {
|
||||
session := newSession(registration.sessionID, m.transport, registration.originProxy)
|
||||
session := newSession(registration.sessionID, m.transport, registration.originProxy, m.log)
|
||||
m.sessions[registration.sessionID] = session
|
||||
registration.resultChan <- session
|
||||
}
|
||||
|
||||
func (m *manager) UnregisterSession(ctx context.Context, sessionID uuid.UUID) error {
|
||||
event := &unregisterSessionEvent{sessionID: sessionID}
|
||||
func (m *manager) UnregisterSession(ctx context.Context, sessionID uuid.UUID, message string, byRemote bool) error {
|
||||
event := &unregisterSessionEvent{
|
||||
sessionID: sessionID,
|
||||
err: &errClosedSession{
|
||||
message: message,
|
||||
byRemote: byRemote,
|
||||
},
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
@@ -115,7 +120,7 @@ func (m *manager) unregisterSession(unregistration *unregisterSessionEvent) {
|
||||
session, ok := m.sessions[unregistration.sessionID]
|
||||
if ok {
|
||||
delete(m.sessions, unregistration.sessionID)
|
||||
session.close()
|
||||
session.close(unregistration.err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ import (
|
||||
|
||||
func TestManagerServe(t *testing.T) {
|
||||
const (
|
||||
sessions = 20
|
||||
msgs = 50
|
||||
sessions = 20
|
||||
msgs = 50
|
||||
remoteUnregisterMsg = "eyeball closed connection"
|
||||
)
|
||||
log := zerolog.Nop()
|
||||
transport := &mockQUICTransport{
|
||||
@@ -89,7 +90,13 @@ func TestManagerServe(t *testing.T) {
|
||||
|
||||
sessionDone := make(chan struct{})
|
||||
go func() {
|
||||
session.Serve(ctx, time.Minute*2)
|
||||
closedByRemote, err := session.Serve(ctx, time.Minute*2)
|
||||
closeSession := &errClosedSession{
|
||||
message: remoteUnregisterMsg,
|
||||
byRemote: true,
|
||||
}
|
||||
require.Equal(t, closeSession, err)
|
||||
require.True(t, closedByRemote)
|
||||
close(sessionDone)
|
||||
}()
|
||||
|
||||
@@ -100,7 +107,7 @@ func TestManagerServe(t *testing.T) {
|
||||
// Make sure eyeball and origin have received all messages before unregistering the session
|
||||
require.NoError(t, reqErrGroup.Wait())
|
||||
|
||||
require.NoError(t, mg.UnregisterSession(ctx, sessionID))
|
||||
require.NoError(t, mg.UnregisterSession(ctx, sessionID, remoteUnregisterMsg, true))
|
||||
<-sessionDone
|
||||
|
||||
return nil
|
||||
|
||||
+49
-27
@@ -2,17 +2,23 @@ package datagramsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCloseIdleAfter = time.Second * 210
|
||||
)
|
||||
|
||||
// Each Session is a bidirectional pipe of datagrams between transport and dstConn
|
||||
func SessionIdleErr(timeout time.Duration) error {
|
||||
return fmt.Errorf("session idle for %v", timeout)
|
||||
}
|
||||
|
||||
// Session is a bidirectional pipe of datagrams between transport and dstConn
|
||||
// Currently the only implementation of transport is quic DatagramMuxer
|
||||
// Destination can be a connection with origin or with eyeball
|
||||
// When the destination is origin:
|
||||
@@ -24,47 +30,56 @@ const (
|
||||
// - Datagrams from cloudflared are read by Manager from the transport. Manager finds the corresponding Session and calls the
|
||||
// write method of the Session to send to eyeball
|
||||
type Session struct {
|
||||
id uuid.UUID
|
||||
ID uuid.UUID
|
||||
transport transport
|
||||
dstConn io.ReadWriteCloser
|
||||
// activeAtChan is used to communicate the last read/write time
|
||||
activeAtChan chan time.Time
|
||||
doneChan chan struct{}
|
||||
closeChan chan error
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser) *Session {
|
||||
func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser, log *zerolog.Logger) *Session {
|
||||
return &Session{
|
||||
id: id,
|
||||
ID: id,
|
||||
transport: transport,
|
||||
dstConn: dstConn,
|
||||
// activeAtChan has low capacity. It can be full when there are many concurrent read/write. markActive() will
|
||||
// drop instead of blocking because last active time only needs to be an approximation
|
||||
activeAtChan: make(chan time.Time, 2),
|
||||
doneChan: make(chan struct{}),
|
||||
// capacity is 2 because close() and dstToTransport routine in Serve() can write to this channel
|
||||
closeChan: make(chan error, 2),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) error {
|
||||
serveCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
go s.waitForCloseCondition(serveCtx, closeAfterIdle)
|
||||
// QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975
|
||||
// This makes it safe to share readBuffer between iterations
|
||||
readBuffer := make([]byte, s.transport.MTU())
|
||||
for {
|
||||
if err := s.dstToTransport(readBuffer); err != nil {
|
||||
return err
|
||||
func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) (closedByRemote bool, err error) {
|
||||
go func() {
|
||||
// QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975
|
||||
// This makes it safe to share readBuffer between iterations
|
||||
const maxPacketSize = 1500
|
||||
readBuffer := make([]byte, maxPacketSize)
|
||||
for {
|
||||
if err := s.dstToTransport(readBuffer); err != nil {
|
||||
s.closeChan <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
err = s.waitForCloseCondition(ctx, closeAfterIdle)
|
||||
if closeSession, ok := err.(*errClosedSession); ok {
|
||||
closedByRemote = closeSession.byRemote
|
||||
}
|
||||
return closedByRemote, err
|
||||
}
|
||||
|
||||
func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time.Duration) {
|
||||
func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time.Duration) error {
|
||||
// Closing dstConn cancels read so dstToTransport routine in Serve() can return
|
||||
defer s.dstConn.Close()
|
||||
if closeAfterIdle == 0 {
|
||||
// provide deafult is caller doesn't specify one
|
||||
closeAfterIdle = defaultCloseIdleAfter
|
||||
}
|
||||
// Closing dstConn cancels read so Serve function can return
|
||||
defer s.dstConn.Close()
|
||||
|
||||
checkIdleFreq := closeAfterIdle / 8
|
||||
checkIdleTicker := time.NewTicker(checkIdleFreq)
|
||||
@@ -74,14 +89,14 @@ func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-s.doneChan:
|
||||
return
|
||||
return ctx.Err()
|
||||
case reason := <-s.closeChan:
|
||||
return reason
|
||||
// TODO: TUN-5423 evaluate if using atomic is more efficient
|
||||
case now := <-checkIdleTicker.C:
|
||||
// The session is considered inactive if current time is after (last active time + allowed idle time)
|
||||
if now.After(activeAt.Add(closeAfterIdle)) {
|
||||
return
|
||||
return SessionIdleErr(closeAfterIdle)
|
||||
}
|
||||
case activeAt = <-s.activeAtChan: // Update last active time
|
||||
}
|
||||
@@ -92,8 +107,15 @@ func (s *Session) dstToTransport(buffer []byte) error {
|
||||
n, err := s.dstConn.Read(buffer)
|
||||
s.markActive()
|
||||
if n > 0 {
|
||||
if err := s.transport.SendTo(s.id, buffer[:n]); err != nil {
|
||||
return err
|
||||
if n <= int(s.transport.MTU()) {
|
||||
err = s.transport.SendTo(s.ID, buffer[:n])
|
||||
} else {
|
||||
// drop packet for now, eventually reply with ICMP for PMTUD
|
||||
s.log.Debug().
|
||||
Str("session", s.ID.String()).
|
||||
Int("len", n).
|
||||
Uint("mtu", s.transport.MTU()).
|
||||
Msg("dropped packet exceeding MTU")
|
||||
}
|
||||
}
|
||||
return err
|
||||
@@ -113,6 +135,6 @@ func (s *Session) markActive() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) close() {
|
||||
close(s.doneChan)
|
||||
func (s *Session) close(err *errClosedSession) {
|
||||
s.closeChan <- err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -31,6 +32,12 @@ func TestCloseIdle(t *testing.T) {
|
||||
}
|
||||
|
||||
func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.Duration) {
|
||||
var (
|
||||
localCloseReason = &errClosedSession{
|
||||
message: "connection closed by origin",
|
||||
byRemote: false,
|
||||
}
|
||||
)
|
||||
sessionID := uuid.New()
|
||||
cfdConn, originConn := net.Pipe()
|
||||
payload := testPayload(sessionID)
|
||||
@@ -38,12 +45,24 @@ func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.D
|
||||
reqChan: newDatagramChannel(1),
|
||||
respChan: newDatagramChannel(1),
|
||||
}
|
||||
session := newSession(sessionID, transport, cfdConn)
|
||||
log := zerolog.Nop()
|
||||
session := newSession(sessionID, transport, cfdConn, &log)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sessionDone := make(chan struct{})
|
||||
go func() {
|
||||
session.Serve(ctx, closeAfterIdle)
|
||||
closedByRemote, err := session.Serve(ctx, closeAfterIdle)
|
||||
switch closeBy {
|
||||
case closeByContext:
|
||||
require.Equal(t, context.Canceled, err)
|
||||
require.False(t, closedByRemote)
|
||||
case closeByCallingClose:
|
||||
require.Equal(t, localCloseReason, err)
|
||||
require.Equal(t, localCloseReason.byRemote, closedByRemote)
|
||||
case closeByTimeout:
|
||||
require.Equal(t, SessionIdleErr(closeAfterIdle), err)
|
||||
require.False(t, closedByRemote)
|
||||
}
|
||||
close(sessionDone)
|
||||
}()
|
||||
|
||||
@@ -64,7 +83,7 @@ func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.D
|
||||
case closeByContext:
|
||||
cancel()
|
||||
case closeByCallingClose:
|
||||
session.close()
|
||||
session.close(localCloseReason)
|
||||
}
|
||||
|
||||
<-sessionDone
|
||||
@@ -102,7 +121,8 @@ func testActiveSessionNotClosed(t *testing.T, readFromDst bool, writeToDst bool)
|
||||
reqChan: newDatagramChannel(100),
|
||||
respChan: newDatagramChannel(100),
|
||||
}
|
||||
session := newSession(sessionID, transport, cfdConn)
|
||||
log := zerolog.Nop()
|
||||
session := newSession(sessionID, transport, cfdConn, &log)
|
||||
|
||||
startTime := time.Now()
|
||||
activeUntil := startTime.Add(activeTime)
|
||||
@@ -164,7 +184,7 @@ func testActiveSessionNotClosed(t *testing.T, readFromDst bool, writeToDst bool)
|
||||
|
||||
func TestMarkActiveNotBlocking(t *testing.T) {
|
||||
const concurrentCalls = 50
|
||||
session := newSession(uuid.New(), nil, nil)
|
||||
session := newSession(uuid.New(), nil, nil, nil)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(concurrentCalls)
|
||||
for i := 0; i < concurrentCalls; i++ {
|
||||
|
||||
@@ -8,6 +8,6 @@ type transport interface {
|
||||
SendTo(sessionID uuid.UUID, payload []byte) error
|
||||
// ReceiveFrom reads the next datagram from the transport
|
||||
ReceiveFrom() (uuid.UUID, []byte, error)
|
||||
// Max transmission unit of the transport
|
||||
// Max transmission unit to receive from the transport
|
||||
MTU() uint
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func (mt *mockQUICTransport) ReceiveFrom() (uuid.UUID, []byte, error) {
|
||||
}
|
||||
|
||||
func (mt *mockQUICTransport) MTU() uint {
|
||||
return 1220
|
||||
return 1217
|
||||
}
|
||||
|
||||
func (mt *mockQUICTransport) newRequest(ctx context.Context, sessionID uuid.UUID, payload []byte) error {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
package h2mux
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !cgo
|
||||
// +build !cgo
|
||||
|
||||
package h2mux
|
||||
|
||||
+3
-3
@@ -83,15 +83,15 @@ func ServeMetrics(
|
||||
return err
|
||||
}
|
||||
|
||||
func RegisterBuildInfo(buildTime string, version string) {
|
||||
func RegisterBuildInfo(buildType, buildTime, version string) {
|
||||
buildInfo := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
// Don't namespace build_info, since we want it to be consistent across all Cloudflare services
|
||||
Name: "build_info",
|
||||
Help: "Build and version information",
|
||||
},
|
||||
[]string{"goversion", "revision", "version"},
|
||||
[]string{"goversion", "type", "revision", "version"},
|
||||
)
|
||||
prometheus.MustRegister(buildInfo)
|
||||
buildInfo.WithLabelValues(runtime.Version(), buildTime, version).Set(1)
|
||||
buildInfo.WithLabelValues(runtime.Version(), buildType, buildTime, version).Set(1)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package origin
|
||||
|
||||
+6
-3
@@ -9,7 +9,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MaxDatagramFrameSize = 1220
|
||||
// Max datagram frame size is limited to 1220 https://github.com/lucas-clemente/quic-go/blob/v0.24.0/internal/protocol/params.go#L138
|
||||
// However, 3 more bytes are reserved https://github.com/lucas-clemente/quic-go/blob/v0.24.0/internal/wire/datagram_frame.go#L61
|
||||
MaxDatagramFrameSize = 1217
|
||||
sessionIDLen = len(uuid.UUID{})
|
||||
)
|
||||
|
||||
@@ -34,7 +36,7 @@ func NewDatagramMuxer(quicSession quic.Session) (*DatagramMuxer, error) {
|
||||
func (dm *DatagramMuxer) SendTo(sessionID uuid.UUID, payload []byte) error {
|
||||
if len(payload) > MaxDatagramFrameSize-sessionIDLen {
|
||||
// TODO: TUN-5302 return ICMP packet too big message
|
||||
return fmt.Errorf("origin UDP payload has %d bytes, which exceeds transport MTU %d", len(payload), MaxDatagramFrameSize-sessionIDLen)
|
||||
return fmt.Errorf("origin UDP payload has %d bytes, which exceeds transport MTU %d", len(payload), dm.MTU())
|
||||
}
|
||||
msgWithID, err := SuffixSessionID(sessionID, payload)
|
||||
if err != nil {
|
||||
@@ -57,8 +59,9 @@ func (dm *DatagramMuxer) ReceiveFrom() (uuid.UUID, []byte, error) {
|
||||
return ExtractSessionID(msg)
|
||||
}
|
||||
|
||||
// Maximum application payload to send to / receive from QUIC datagram frame
|
||||
func (dm *DatagramMuxer) MTU() uint {
|
||||
return MaxDatagramFrameSize
|
||||
return uint(MaxDatagramFrameSize - sessionIDLen)
|
||||
}
|
||||
|
||||
// Each QUIC datagram should be suffixed with session ID.
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -39,3 +49,91 @@ func TestSuffixSessionIDError(t *testing.T) {
|
||||
_, err = SuffixSessionID(testSessionID, msg)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMaxDatagramPayload(t *testing.T) {
|
||||
payload := make([]byte, MaxDatagramFrameSize-sessionIDLen)
|
||||
|
||||
quicConfig := &quic.Config{
|
||||
KeepAlive: true,
|
||||
EnableDatagrams: true,
|
||||
}
|
||||
quicListener := newQUICListener(t, quicConfig)
|
||||
defer quicListener.Close()
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(context.Background())
|
||||
// Run edge side of datagram muxer
|
||||
errGroup.Go(func() error {
|
||||
// Accept quic connection
|
||||
quicSession, err := quicListener.Accept(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer, err := NewDatagramMuxer(quicSession)
|
||||
require.NoError(t, err)
|
||||
|
||||
sessionID, receivedPayload, err := muxer.ReceiveFrom()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testSessionID, sessionID)
|
||||
require.True(t, bytes.Equal(payload, receivedPayload))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Run cloudflared side of datagram muxer
|
||||
errGroup.Go(func() error {
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
// Establish quic connection
|
||||
quicSession, err := quic.DialAddrEarly(quicListener.Addr().String(), tlsClientConfig, quicConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer, err := NewDatagramMuxer(quicSession)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = muxer.SendTo(testSessionID, payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Payload larger than transport MTU, should return an error
|
||||
largePayload := append(payload, byte(1))
|
||||
err = muxer.SendTo(testSessionID, largePayload)
|
||||
require.Error(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
func newQUICListener(t *testing.T, config *quic.Config) quic.Listener {
|
||||
// Create a simple tls config.
|
||||
tlsConfig := generateTLSConfig()
|
||||
|
||||
listener, err := quic.ListenAddr("127.0.0.1:0", tlsConfig, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
func generateTLSConfig() *tls.Config {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
template := x509.Certificate{SerialNumber: big.NewInt(1)}
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,13 +155,16 @@ func NewRPCServerStream(stream io.ReadWriteCloser, protocol ProtocolSignature) (
|
||||
}
|
||||
|
||||
func (s *RPCServerStream) Serve(sessionManager tunnelpogs.SessionManager, logger *zerolog.Logger) error {
|
||||
rpcTransport := tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(s))
|
||||
// RPC logs are very robust, create a new logger that only logs error to reduce noise
|
||||
rpcLogger := logger.Level(zerolog.ErrorLevel)
|
||||
rpcTransport := tunnelrpc.NewTransportLogger(&rpcLogger, rpc.StreamTransport(s))
|
||||
defer rpcTransport.Close()
|
||||
|
||||
main := tunnelpogs.SessionManager_ServerToClient(sessionManager)
|
||||
rpcConn := rpc.NewConn(
|
||||
rpcTransport,
|
||||
rpc.MainInterface(main.Client),
|
||||
tunnelrpc.ConnLog(&rpcLogger),
|
||||
)
|
||||
defer rpcConn.Close()
|
||||
|
||||
@@ -179,7 +182,7 @@ func DetermineProtocol(stream io.Reader) (ProtocolSignature, error) {
|
||||
case RPCStreamProtocolSignature:
|
||||
return RPCStreamProtocolSignature, nil
|
||||
default:
|
||||
return ProtocolSignature{}, fmt.Errorf("Unknown signature %v", signature)
|
||||
return ProtocolSignature{}, fmt.Errorf("unknown signature %v", signature)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,8 +251,8 @@ func (rcs *RPCClientStream) RegisterUdpSession(ctx context.Context, sessionID uu
|
||||
return resp.Err
|
||||
}
|
||||
|
||||
func (rcs *RPCClientStream) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error {
|
||||
return rcs.client.UnregisterUdpSession(ctx, sessionID)
|
||||
func (rcs *RPCClientStream) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error {
|
||||
return rcs.client.UnregisterUdpSession(ctx, sessionID, message)
|
||||
}
|
||||
|
||||
func (rcs *RPCClientStream) Close() {
|
||||
|
||||
+17
-11
@@ -114,11 +114,13 @@ func TestRegisterUdpSession(t *testing.T) {
|
||||
clientStream := mockRPCStream{clientReader, clientWriter}
|
||||
serverStream := mockRPCStream{serverReader, serverWriter}
|
||||
|
||||
unregisterMessage := "closed by eyeball"
|
||||
rpcServer := mockRPCServer{
|
||||
sessionID: uuid.New(),
|
||||
dstIP: net.IP{172, 16, 0, 1},
|
||||
dstPort: 8000,
|
||||
closeIdleAfter: testCloseIdleAfterHint,
|
||||
sessionID: uuid.New(),
|
||||
dstIP: net.IP{172, 16, 0, 1},
|
||||
dstPort: 8000,
|
||||
closeIdleAfter: testCloseIdleAfterHint,
|
||||
unregisterMessage: unregisterMessage,
|
||||
}
|
||||
logger := zerolog.Nop()
|
||||
sessionRegisteredChan := make(chan struct{})
|
||||
@@ -142,20 +144,21 @@ func TestRegisterUdpSession(t *testing.T) {
|
||||
// Different sessionID, the RPC server should reject the registraion
|
||||
assert.Error(t, rpcClientStream.RegisterUdpSession(context.Background(), uuid.New(), rpcServer.dstIP, rpcServer.dstPort, testCloseIdleAfterHint))
|
||||
|
||||
assert.NoError(t, rpcClientStream.UnregisterUdpSession(context.Background(), rpcServer.sessionID))
|
||||
assert.NoError(t, rpcClientStream.UnregisterUdpSession(context.Background(), rpcServer.sessionID, unregisterMessage))
|
||||
|
||||
// Different sessionID, the RPC server should reject the unregistraion
|
||||
assert.Error(t, rpcClientStream.UnregisterUdpSession(context.Background(), uuid.New()))
|
||||
assert.Error(t, rpcClientStream.UnregisterUdpSession(context.Background(), uuid.New(), unregisterMessage))
|
||||
|
||||
rpcClientStream.Close()
|
||||
<-sessionRegisteredChan
|
||||
}
|
||||
|
||||
type mockRPCServer struct {
|
||||
sessionID uuid.UUID
|
||||
dstIP net.IP
|
||||
dstPort uint16
|
||||
closeIdleAfter time.Duration
|
||||
sessionID uuid.UUID
|
||||
dstIP net.IP
|
||||
dstPort uint16
|
||||
closeIdleAfter time.Duration
|
||||
unregisterMessage string
|
||||
}
|
||||
|
||||
func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error {
|
||||
@@ -174,10 +177,13 @@ func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UU
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s mockRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error {
|
||||
func (s mockRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error {
|
||||
if s.sessionID != sessionID {
|
||||
return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID)
|
||||
}
|
||||
if s.unregisterMessage != message {
|
||||
return fmt.Errorf("expect unregister message %s, got %s", s.unregisterMessage, message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package sshgen
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//+build darwin
|
||||
//go:build darwin
|
||||
// +build darwin
|
||||
|
||||
package token
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//+build !windows,!darwin,!linux,!netbsd,!freebsd,!openbsd
|
||||
//go:build !windows && !darwin && !linux && !netbsd && !freebsd && !openbsd
|
||||
// +build !windows,!darwin,!linux,!netbsd,!freebsd,!openbsd
|
||||
|
||||
package token
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//+build linux freebsd openbsd netbsd
|
||||
//go:build linux || freebsd || openbsd || netbsd
|
||||
// +build linux freebsd openbsd netbsd
|
||||
|
||||
package token
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//+build windows
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package token
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package token
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
type SessionManager interface {
|
||||
RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) error
|
||||
UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error
|
||||
UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error
|
||||
}
|
||||
|
||||
type SessionManager_PogsImpl struct {
|
||||
@@ -76,7 +76,12 @@ func (i SessionManager_PogsImpl) UnregisterUdpSession(p tunnelrpc.SessionManager
|
||||
return err
|
||||
}
|
||||
|
||||
return i.impl.UnregisterUdpSession(p.Ctx, sessionID)
|
||||
message, err := p.Params.Message()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return i.impl.UnregisterUdpSession(p.Ctx, sessionID, message)
|
||||
}
|
||||
|
||||
type RegisterUdpSessionResponse struct {
|
||||
@@ -137,12 +142,15 @@ func (c SessionManager_PogsClient) RegisterUdpSession(ctx context.Context, sessi
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c SessionManager_PogsClient) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error {
|
||||
func (c SessionManager_PogsClient) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error {
|
||||
client := tunnelrpc.SessionManager{Client: c.Client}
|
||||
promise := client.UnregisterUdpSession(ctx, func(p tunnelrpc.SessionManager_unregisterUdpSession_Params) error {
|
||||
if err := p.SetSessionId(sessionID[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.SetMessage(message); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
_, err := promise.Struct()
|
||||
|
||||
@@ -150,5 +150,5 @@ struct RegisterUdpSessionResponse {
|
||||
interface SessionManager {
|
||||
# Let the edge decide closeAfterIdle to make sure cloudflared doesn't close session before the edge closes its side
|
||||
registerUdpSession @0 (sessionId :Data, dstIp :Data, dstPort: UInt16, closeAfterIdleHint: Int64) -> (result :RegisterUdpSessionResponse);
|
||||
unregisterUdpSession @1 (sessionId :Data) -> ();
|
||||
unregisterUdpSession @1 (sessionId :Data, message: Text) -> ();
|
||||
}
|
||||
+221
-201
@@ -3485,7 +3485,7 @@ func (c SessionManager) UnregisterUdpSession(ctx context.Context, params func(Se
|
||||
Options: capnp.NewCallOptions(opts),
|
||||
}
|
||||
if params != nil {
|
||||
call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1}
|
||||
call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 2}
|
||||
call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_unregisterUdpSession_Params{Struct: s}) }
|
||||
}
|
||||
return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))}
|
||||
@@ -3743,12 +3743,12 @@ type SessionManager_unregisterUdpSession_Params struct{ capnp.Struct }
|
||||
const SessionManager_unregisterUdpSession_Params_TypeID = 0x96b74375ce9b0ef6
|
||||
|
||||
func NewSessionManager_unregisterUdpSession_Params(s *capnp.Segment) (SessionManager_unregisterUdpSession_Params, error) {
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
|
||||
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
|
||||
return SessionManager_unregisterUdpSession_Params{st}, err
|
||||
}
|
||||
|
||||
func NewRootSessionManager_unregisterUdpSession_Params(s *capnp.Segment) (SessionManager_unregisterUdpSession_Params, error) {
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
|
||||
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
|
||||
return SessionManager_unregisterUdpSession_Params{st}, err
|
||||
}
|
||||
|
||||
@@ -3776,12 +3776,31 @@ func (s SessionManager_unregisterUdpSession_Params) SetSessionId(v []byte) error
|
||||
return s.Struct.SetData(0, v)
|
||||
}
|
||||
|
||||
func (s SessionManager_unregisterUdpSession_Params) Message() (string, error) {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return p.Text(), err
|
||||
}
|
||||
|
||||
func (s SessionManager_unregisterUdpSession_Params) HasMessage() bool {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return p.IsValid() || err != nil
|
||||
}
|
||||
|
||||
func (s SessionManager_unregisterUdpSession_Params) MessageBytes() ([]byte, error) {
|
||||
p, err := s.Struct.Ptr(1)
|
||||
return p.TextBytes(), err
|
||||
}
|
||||
|
||||
func (s SessionManager_unregisterUdpSession_Params) SetMessage(v string) error {
|
||||
return s.Struct.SetText(1, v)
|
||||
}
|
||||
|
||||
// SessionManager_unregisterUdpSession_Params_List is a list of SessionManager_unregisterUdpSession_Params.
|
||||
type SessionManager_unregisterUdpSession_Params_List struct{ capnp.List }
|
||||
|
||||
// NewSessionManager_unregisterUdpSession_Params creates a new list of SessionManager_unregisterUdpSession_Params.
|
||||
func NewSessionManager_unregisterUdpSession_Params_List(s *capnp.Segment, sz int32) (SessionManager_unregisterUdpSession_Params_List, error) {
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz)
|
||||
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz)
|
||||
return SessionManager_unregisterUdpSession_Params_List{l}, err
|
||||
}
|
||||
|
||||
@@ -3861,203 +3880,204 @@ func (p SessionManager_unregisterUdpSession_Results_Promise) Struct() (SessionMa
|
||||
return SessionManager_unregisterUdpSession_Results{s}, err
|
||||
}
|
||||
|
||||
const schema_db8274f9144abc7e = "x\xda\xccY{p\x14e\xb6?\xa7{&\x9d@\x86" +
|
||||
"IW\x0f\x04\xa6\xe4\xe6^\x0a\xcaK\x14\x14\xb8\xdeB" +
|
||||
"\xae\xde\x04L\xb8&\xf2H\xcf\x90\xbb\x96\xa0eg\xe6" +
|
||||
"#tv\xa6{\xe8\xee\x89\x04A\x1e\x82\x88\xe5\x0b\x04" +
|
||||
"E\x94\x95\xc5r\xb7D\xdd\x85U\xd7eKke\xd7" +
|
||||
"\x17\xa5\xa8X\xb8\x05\x8a\xb5\xab\xc8>(\\W\xc4\xb5" +
|
||||
"\xdcu\xed\xad\xd3=\xfd\xc8$$A\xf6\x8f\xfdor" +
|
||||
"\xfa\xfb\xcew\xce\xef\xfc\xbes\xcewr\xe9w*\x1b" +
|
||||
"\xb9)\xd1\xda\x18\x80\xbc-Za\xb3\xfaw\x96\xef\x9c" +
|
||||
"\xf0\xab\xb5 '\x11\xed[\x9eoM|e\xad}\x1f" +
|
||||
"\xa2\xbc\x000mi\xc5r\x94\xd6W\x08\x00\xd2\x9a\x8a" +
|
||||
"\xdf\x03\xda\xb7\x8d\xda\xf3\xc8c\xcd[n\x051\xc9\x07" +
|
||||
"\x8b\x01\xa71\xa1\x15\xa5\x1e\x81V\x16\x85\x0d\xd2\xbb\xf4" +
|
||||
"\xcb\xbeF\xbcda\xe2\xed7iuXu\x84T\xbf" +
|
||||
" \xd4\xa3t\xd0\xd9p@ \xd5W\xe4\xdf\xda\xf5\xdf" +
|
||||
"[__\x07b\x92\xeb\xa5\xfa\xd9\xca\xe5(\x1d\xa8\xa4" +
|
||||
"\x95/W\xce\x07\xb4?\xdf2\xfa\x89\xef\xbf\xf9\xdaz" +
|
||||
"\x10/D(Y\xfaA\xe5{\x08(}V\xf9c@" +
|
||||
"\xfb\xe0\x17\x0b\xcf<\xf3\xcae\xb7\x818\x91\x16 -" +
|
||||
"\xd8T5\x8e\x03\x94\x1e\xafj\x00\xb4O\x9e\xfa\xdb\x86" +
|
||||
"\x9b'\xce\xbb\x17\xe4\x89\xc8\x01D9Zq\xa0*I" +
|
||||
"+>\xac\"k\x1af\x1f\xdc\x97\x9cv\xff\x962\xd3" +
|
||||
"\x9d\x85\xfb\x87\xd5\xa3th\x18\x19tp\xd8M\x80\xf6" +
|
||||
"_F<\xf4f\xf1\xaa\xe7\xee\x0f\x9f7ex=i" +
|
||||
"k\x19N\xe7\x8d\xeb\x9ep\xe3/_~\xfa\x01\x90'" +
|
||||
"!\xda\xc7:.z\x97\xdf\xb1\xfb}hG\x81\x8e\x9f" +
|
||||
"\x96\x1f\xbe\x8b\x8c_9\x9c\x94\xbdu\xf1\xf3?\xbf\xf7" +
|
||||
"\xe9\x0d\x0f\x81|!\"\x80\x03\xd6\x87\xc3\xffJ\x0b\xbe" +
|
||||
"p\x94m9\xf2\xc2\xbc\xfc\xa6\xed\xbb\\\xf7\x9d\xefc" +
|
||||
"\xab9\x0e\"\xf6\xba\x96/\xf3\xed\x8f\xa6\x1f-\x01\x13" +
|
||||
"\xa5Ob\xf5i\x04\x9c6\xa1\xba\x0e\x01\xed\xcb\xde;" +
|
||||
"1\x7f\xeeO\x16\xff0\xb4wfl9\xed\xdd\xb0\xf8" +
|
||||
"\xf4\xfe\x9aT\xfe\x892\x87\x1d_\xae\x8c\xedF\xa9=" +
|
||||
"F\x0e\xcb12\xe1\xa9\x7f\xbb\xa6j\xd9\x89\xd9{@" +
|
||||
"\x9c\xe4\xa9Y\x1aK\x91\x9a\xc8\xf5\xfc7\xca\xb6_<" +
|
||||
"SN'\x07\xb8|\xac\x03\xa55\xa4g\xda\xca\x98c" +
|
||||
"\xcf\x1d\xfb\xb7_T\xf9\xc8\xe7\xcf\xf6\x07\xf3c#:" +
|
||||
"P\xda7\x82N}v\x04!3\xb2\x05\x8f\xbd8%" +
|
||||
"\xf2\\8\xee#\xe3'\x09\x99\x89q\x8a\xfb\xd8Of" +
|
||||
"\xc5\xb4O\xd7\xbeX\xa6\xcdYx \xde\x8a\xd2\x07q" +
|
||||
"\xd2v\xd4Y\xdc\xba\xf0\xbe\xcd\xd1\x13\xf7\xbdJ\x96\x86" +
|
||||
"\x08\x17%\xa2M+\xd6\x18(m\xac\xa1\x9f\xebkj" +
|
||||
"y@;\xb9\xe7\x7f~4+{\xf4\xf5~,\x95\xa2" +
|
||||
"\x89\xd3\x92\x98\xa0_\xb1\x04\x19z|\xd2\xde\x9b\xffx" +
|
||||
"\xd7\xa1\xc3%C\x1d\x0c\xd5\x84\x13\xc2\x95\x09\xc2\xcfg" +
|
||||
"@\x19J\xce\xca\x1d\x89.\x94\xf6:\xea\x9erVs" +
|
||||
"'\x941\xab\x7f\xfd\xbf\xc7BA\xdb\x9b\xf8\x08!b" +
|
||||
"\xcf\xfb\xff\x85]U+\x8f\x1f\x0f\x1f\xf4X\xc2Ad" +
|
||||
"\x9f\xb3\xf5O?8y\xcf\xa9|\xf6w\x0e\xf1<\xcc" +
|
||||
"\x8e&f\x105?K\x10\xd1k\xebb\xcd\xe3\x8e\xb4" +
|
||||
"\x9dtC\xe9\xaa88r\x16-81\x92T\\v" +
|
||||
"\xe3L\xb6h\xfa\xb5'\xfb\\\xf9\xe8\xa8\x19(\x8d\x1c" +
|
||||
"\xe5\x90l\xd4\x06\x94&\xd6\xd6\x02\xd8\xdd?\xddt\xed" +
|
||||
"\x13/\xcd;\xed\xde\x05\xc7\xd81\xb5S\x89\x1aw\xdf" +
|
||||
"\xd24\xff\xf2q\xfbO\x87\x8d\x15k\x89\x9d\xd2\x84Z" +
|
||||
":i\xf1\xf4S\xff7\xe1\xeeWN\xf7G\xc1\xe6\xda" +
|
||||
"z\x94\xdak\x1d\x0a\xd2\xe2Og\x7f\xefp2\x9e<" +
|
||||
"S\x06`\x85\x13\xbc\xda.\x946\xd6:\xc1\xab}\x95" +
|
||||
"hv\xdb\xfb7,{\xe7\xd6\xcf\xbf(\x8f\xb5\xa3\xba" +
|
||||
"gL\x0a\xa5\xbb\xc6\x90\xea\x8dc\x88\x19\x0f,\xf8\xc3" +
|
||||
"\xaaS[G}\xd9\xc7\xe3\x89\xc9.\x94\xaeL\xd2\xca" +
|
||||
"\xcb\x93\x1b\xa4\x07\xe9\x97\xfd\xb6\xf0\xe8\x94\xa6U\xaf\x7f" +
|
||||
"\x15\xba\x0bk\x92\xad\xe4\xf0\xfd\xc2\xc3\xc7W\xff\xe6\x86" +
|
||||
"\xaf\xc3\x0e\xafL~D\x0eoJ\x92\xc3+>}\xf0" +
|
||||
"\xea{\x16=\xf9M8\xb0\xc9\xb5\xb4\xd5*j\x1a\xcb" +
|
||||
"\x19\x85H\xe6\x12\xefgfrF)h\x85\x193\x8b" +
|
||||
"\xd6\x12\xa6YjF\xb1X\x8a5\x98\x05]3Y\x1b" +
|
||||
"\xa2\\\xc3G\x00\"\x08 *]\x00\xf2\x8d<\xca9" +
|
||||
"\x0eE\xc4\x04\x85^TI\xb8\x84G\xd9\xe2P\xe4\xb8" +
|
||||
"\x04e\x1eq\xe98\x009\xc7\xa3\xbc\x8cC\xe4\x13\xc8" +
|
||||
"\x03\x88\xc5\xcd\x00\xf22\x1e\xe5u\x1c\xda\x05f\xe4\x15" +
|
||||
"\x8di\x10\xb7\x9a\x0d\x03\xab\x81\xc3j@\xdb`\x96\xd1" +
|
||||
"\xa3t\xe4 \xceBb\xa1\xeb&\x0bc\xc0a\x0c\xd0" +
|
||||
"^\xa2\x17\x0d\xb3]\xb3P\xcd\xa5\xd8b\x83\x99\xb8\x04" +
|
||||
"+\x80\xc3\x0a\xc0\x81\xdcK3\xd3Tum\xae\xa2)" +
|
||||
"\x9d\xcc\x00 \xcf*\xf9(\x80\x9f\xb4\xd1K\xef\xe2\x94" +
|
||||
"\xed\xc0\x89\x93\x04\x0c20z\xf4\x13\xffc7p\xe2" +
|
||||
"X\xc16X\xa7jZ\xcc\xc0\xf6l\xc1\xd1\xcd\xebZ" +
|
||||
"#\xdaE\xcd\xfd\x80\xccp?\xc4\xe9\xd4Fl\xc3\xc0" +
|
||||
":\xbe\xafuW\xe5T\xa6Y\xf1\x16m\xb1^\x06y" +
|
||||
"k\x7f\x90\xb7\x96 _\x17\x82|\xcd,\x00y\x05\x8f" +
|
||||
"\xf2\xed\x1c\x8a|\x09\xf3\xf5\xf5\x00\xf2j\x1e\xe5;9" +
|
||||
"\xb43\xce!-Y\x00\xf0\xd1\\\xcc\x14\xabh0\x93" +
|
||||
"d#\x00\xdbxt@\x1f\x01\xb8\xaa\x9b\x19d\xbb\x17" +
|
||||
"\x84\xb8bd\x96\xf8\x81\x1a\x00\xe9\xe6e\xaai\xa9Z" +
|
||||
"\xe7\x02G\xde\xd0\xa6\xe7\xd4L\x0fyU\xed\xd89v" +
|
||||
"\x06\x00\xa28\xf2:\x00\xe4Dq\x16@\x83\xda\xa9\xe9" +
|
||||
"\x06\xb3\xb3\xaa\x99\xd15\x8d\x01\x9f\xb1Vu(9E" +
|
||||
"\xcb0\xff\xa0\x8a\xbe\x07\xb9\x07\xa4\x99\xd1\xcd\x8c\xc9J" +
|
||||
"\x88\xbe\xe3\xdb\x14C\xe1\xf3\xa6\\\xed\xe3\xd8|\x1d\x80" +
|
||||
"\xdc\xc4\xa3\xdc\x16\xc2q.\xe18\x87G\xf9\xda\x10\x8e" +
|
||||
"\xed\x84c\x1b\x8f\xf2\"\x0em\xddP;U\xed*\x06" +
|
||||
"\xbc\x11f\xa0iiJ\x9e\x11f%<V\xe9\x05K" +
|
||||
"\xd55\x13k\x82\xfc\x0f\x885!\xa4\x84\xc189\xd9" +
|
||||
"\xa3\x94\xc7(]\x1b\x9fbfQ\xc8Y\xa6\x1c\xf1=" +
|
||||
"\x89\xcd\x00\x90+y\x94\x13\x1c6\x18\xcc,\xe6,\xac" +
|
||||
"\x09\xca\xec?\xe3T\x0f\xbe\x10\x0dS\xfd\xd1p*\x80" +
|
||||
"\x9c\xe5Q.p\x88%\xf4\xf2\xb3B\xd9\x80G\x97\x85" +
|
||||
"K\xb7\x03\xc8\x16\x8f\xf2j\x0em\xd3=\xa4\x050\xeb" +
|
||||
"!Z\x975\xad\x96\x82\xf7\xd7\xaa\xaci\xb5\xe9\x86\x85" +
|
||||
"\x02p(\x00\xf1V7\xd9\xcc\xc5t\xa7Z\xb29v" +
|
||||
"\xb5\xcak\x16F\x81\xc3(\x0cx\xa9\\~\xc4)\xb1" +
|
||||
"\xb9\xb7\xdd\xf3f\"\x91\xe1?y\x94\xff+\xe4\xcd\x14" +
|
||||
"\xcac\x97\xf2(_\xc1\xa1\xadd2zQ\xb3\x16\x00" +
|
||||
"\xaft\x96q>\xcd \x9e1X@\x87\xa1C\xed%" +
|
||||
"\x872\xb0\xe3\x86\x92\xef\x15a\x02\xbb\x9aGyt\xff" +
|
||||
"p\xf9'F\xfbI$t\x812\xc4\xc5\x14sS\xf8" +
|
||||
"d\x83\x99B1g\x91\xff\xd5\xb6\xed\x02@\x91\x1b\xcf" +
|
||||
"\xa3|)\x871\xfc\xc6v\x11\x98\xb49@\xa0\x8e\x19" +
|
||||
"\x86n`MP\xe2J\xc4\xca\x94\x0e@]kb\x96" +
|
||||
"\xa2\xe6\x90H\xef\xf7[e\xf4\x1b\xec\xd6\x06\x88\xb8\xe2" +
|
||||
"\xf1\x0d\xc4\xbd\xdeh\x10yjx\x94/\xe0\xd0\xee4" +
|
||||
"\x94\x0ckc\x06\xaazv\x9e\xa2\xe9i\x9ee\xfaP" +
|
||||
"a\xc4\xb9\x1e\x9arn\x91\x09\xfe\xae\x81\xf7\x1b\xac\x04" +
|
||||
"Bi{[\x9dks\xc2\xb7y\xe5\xb8\xa0\xd4\xf9\x04" +
|
||||
"[\xd3\x11\xe4b?\xdbl$*\xde\xce\xa3\xbc%\x94" +
|
||||
"\xb57Q^\xba\x97G\xf9a\x0e\xc5H$\x81\x11\x00" +
|
||||
"\xf1A\xbaY[x\x94wr\xbd\x0b\"\xebf\x9a\xd5" +
|
||||
"\xa4v\x82\xc0\xcc@J&6\xa9\x9d\x0cx\xf3|3" +
|
||||
"W\xe5 x\xe8\x1d\xa6\x9ec\x16kb\x99\x9cb(" +
|
||||
"\x96\xda\xcd\xdc\xef%2zA\x1d\x88\xb7\xa9>\x17\x83" +
|
||||
"\xf8\x1b\xf7z\x90\x10\x1d\xc6\x05\xe9O`\xa1\xd6a\x00" +
|
||||
"k]\xe5d\x99\xae\xf5\xe1@pcJ<@s\xa0" +
|
||||
"\xea\x16,\x9f_\xb0TA\xd7L\xb2/\x14\xfa\x19\xfd" +
|
||||
"\x85\xde\x08B\xefe\xca\x8dk\xc3\x91/e\xcaM\xdb" +
|
||||
"\x83 \x8b\x11\xce\x8d\xfc\x8e]\x00\xf2N\x1e\xe5'9" +
|
||||
"lp\x8b8\xd6\x04\x8f\xe0R\xb4\xdcR5G\x87\xba" +
|
||||
"\x8c\x92\x0b\xb2\xa9m\xb0BN\xc9\xb0f,\x95e@" +
|
||||
"\x04\x0e\xd1\xa1H\xbe`0\xd3DU\xd7\xe4\xa2\x92S" +
|
||||
"y\xab\xc7o\xa5\xb4b\xbe\xcd`\xdd*\xeaEs\xa6" +
|
||||
"e\xb1\xbcP\xb0\xcc\xa14Z\x01@\x94\x1f\x045g" +
|
||||
"\x96%\xdf\xfa \xf7\xf8\x00M\xa2\xe4{1\x8f\xf2t" +
|
||||
"\x0e\xe3\xc5\xa2\x1a\xe4\xba\x9c\x9eq\xe2\x06\xf1yJ\x9e" +
|
||||
"\xf5\x89v\xc5\xa0w\xb5\xd7M\xf7\x92\xed\xbfRc0" +
|
||||
"p/N\xae;\xcdj\xc8d\xba\x02\x8d<\xcasB" +
|
||||
"&\xb7L\x0d\xf9\xe1\x99<\xb7#\xf0C\xf8.\xeb\xf1" +
|
||||
"\xac\xaacy\xca\xdc\x1e\x98%gf\x82pM\xb0f" +
|
||||
" \xfb\xc2\x17j~\xa1\xce\xf1\x90l\x9c\xee\xd9(\xf5" +
|
||||
"`+@z\x19\xf2\x98^\x87\x81\x99\xd2\x1a\x9c\x05\x90" +
|
||||
"^A\xf2\xdb1\xb0TZ\x8fI\x80\xf4j\x92\xdf\x89" +
|
||||
"\xfe\x9bA\xda\x88\xbb\x01\xd2w\x92x\x1b-\x8f\xf0\xce" +
|
||||
"\x95\x90\xb6:\xea\xb7\x90|'\xc9\xa3\x91\x04F\x01\xa4" +
|
||||
"\x1dX\x0f\x90\xdeF\xf2gH^\xc1%\xb0\x02@\xda" +
|
||||
"\x8b]\x00\xe9=$\x7f\x9e\xe4B4A\xcf&i\x1f" +
|
||||
"\x1a\x00\xe9\x9f\x91\xfc%\x92W\x8eN`%\x80\xb4\xdf" +
|
||||
"\x91\xbfH\xf27H^5&\x81U\x00\xd2\x01\\\x0b" +
|
||||
"\x90~\x8d\xe4\x87I>\x0c\x138\x0c@:\x84\xdb\x01" +
|
||||
"\xd2\x87I\xfe[\x92\x0f\xafH\xe0p\x00\xe9\x03\xc7\x9e" +
|
||||
"#$\xff\x98\xe4\xd5\x91\x04V\x03H\x1f\xe2.\x80\xf4" +
|
||||
"\xc7$\xff3\xc9cB\x02c\x00\xd2'\x8e_\xa7H" +
|
||||
"^\xc9\x95\xb5\xec\x1e\xa3\xca\xfar^7\xfd\x90\xb1\xd2" +
|
||||
"\x1dG\x97\xeemz\x9czo\x8c\x07C0@\x8c\x03" +
|
||||
"\xda\x05]\xcf\xcd\xeb\xcd\xd4\xb8\xa5t\x9a\xde\x1b\xa0&" +
|
||||
"\x98K\x00\x92\xd0\xaf\xfb\x10\xd7\xb5\x96\xac\x9f\x08\xca\xb3" +
|
||||
"\x8eg\x89j\xce,Zz\xb1\x00uY\xc5bY?" +
|
||||
"\xe7\x18Em\xb6\xa1\xe7\x17 3\xf2\xaa\xa6\xe4\x06\xc9" +
|
||||
"FU\xc0a\x15\x94R\x82\xa7{\xe0\xd4t\xf6\x17\x8d" +
|
||||
"\xcfh\xae\x9c\xd1u\x85\x19\x0b\x94\xce\xa1\xe4\xa9\xa9A" +
|
||||
"\xe7\x18\xd7B\x09\xa9\xae[\xc9\x15\xbfMz\xea\xddJ" +
|
||||
"\xa4\x1a\xdcVd\xb0~\xdf\x1bS\x0c\x9eJz7\x84" +
|
||||
"\xbd\x0b*\x86&\x88t\x0eW\xd2?d\xf3;\x99\xe5" +
|
||||
"\xfe\xa2\x87+=\x1b\x84p\x99?\xb7\xdd)f\xc6\x87" +
|
||||
"\xe2z0\xce\x19\xfc\xa9\xd3O\xe1\xef\xa7\xec{=g" +
|
||||
"\xe8\xb9C\xb1_\xc4\xa3\xbc$\x14{\xd6\xda\xcfs'" +
|
||||
"\x15\xcc9D\x9e+\x0d:\xa8P\x14x\x94Wp\x18" +
|
||||
"\xa7w)\xd6\x04s\xdf^F\xf7~\x8b\x13\x15Z\xb4" +
|
||||
",\x03\\\xe6\xb19T>\xfc\x09\xe8\xe0\xdd\xd9\xd0\xdc" +
|
||||
"\xf6\xba\xdeA\x01\xf7\xa7\x8ae'\x9f\xf5\xc9\xd5\xe0\x1e" +
|
||||
"J<\x1b\xed\x8cX\xbc\x09+z\xb3:q\xefr\xe0" +
|
||||
"\xc4\xc7\x05\x0c\xa6\x90\xe8\x0d\x1d\xc5\x1d\x06p\xe2V\x01" +
|
||||
"9\x7ff\x8d\xdelZ\xdcx\x07p\xe2z\x01y\x7f" +
|
||||
"\xe4\x8c\xde\xb4kJ\xcf0\x04N\\)`\xc4\x1f\xe5" +
|
||||
"\xa37+\x13\x97v\x01'\xaa\x02F\xfdi6z\xe3" +
|
||||
"T\xf1\xfa\xb5\xc0\x89\xed\xc1L\x07\x1a\\?\x1a\xd1\xf6" +
|
||||
"8\x0au\x0eK{Ox\xdcU\x00\x8dh{=0" +
|
||||
"\x7f\xb6&\xd8Y\xe5\x0d) \x9eQ,\xd6H\xcd\x99" +
|
||||
"{\xff\xb1\x94\x00\xa0\x11\xe5\x08\x86F\x85\x00\xe7\xfb\xbe" +
|
||||
"L\xb1:'\xce\xdf\xb6e\xf2\xf6\x7f\xcb\x94\xc4\xf7g" +
|
||||
"5\x9d\xe3\x0f\xbbBz\xbbB\x0f\xdfA\x1a\xbf\xc8\xd9" +
|
||||
"\xbc\xf0\xc8\x1f\xa7\xcd\xa4\xff\xdf}\xfd\x87\xa8qz\x83" +
|
||||
"G\xf9H\xe8Z\xbfK\xc2\xb7y\x94\x8f\x85\x1a\xa7\xa3" +
|
||||
"t\xd7\x8f\xf0(\x9f\x09\xe6\x97\x9f\xdd\x01 \x9f\xe11" +
|
||||
"\x15jD\xc4\xbf\xd3\xc2\xaf\xa9\\;m\x08\xbamH" +
|
||||
"\x147\x03\xa4+\xa9\x8c'\x9c6$\xe2\xb6!\"v" +
|
||||
"\x00\xa4kH~A\xb8\x0d\x19\x83\xd7\x01\xa4G\x93|" +
|
||||
"<\xf6~\xd7\x08E#h\xd4rz\xe7\x1cU\xeb\xb7" +
|
||||
"\xb6y\x03U\xb4f+j\xaeh0\x08Jk)\xd9" +
|
||||
"4\x85\xaa\xbd;iu\x87*i\"a\x16M\x7f\xe0" +
|
||||
"r\x0e/\xca!U\x9ef\xc3\xd0\xd1(kb\xa7\x06" +
|
||||
"M\xac\xdf\xc3R/~5\x8f\xf2\x02\x0aE\xa3\x1b\x0a" +
|
||||
"\xb9#h\xbb\xeb2J\xd1d}|\x00\x9e\x19\xfe\x14" +
|
||||
"\xc0\\\xa2\x17s\xd9\x14\x03\xc12z\xca \x18\xb4\x99" +
|
||||
"M\xb3\xb8\x97\xb9\xdc\xe1\xb0\xf7\x8f\x0e\xf4\xfe\x9f\x11\x1a" +
|
||||
"\x0e{\x13z\xf4\xfem\xd5w8\xeca\xd0g8\xec" +
|
||||
"~p8\xda{8|\x1e\xcfW\xb7\x8c\x852\xc69" +
|
||||
"\xcdL\x87<j\xf4\xff\xb3[v\xd3\xab\xcewL\xe0" +
|
||||
"\x15\xa4\x7f\x04\x00\x00\xff\xff\xa5\x0ed\xc9"
|
||||
const schema_db8274f9144abc7e = "x\xda\xccY}p\x14e\x9a\x7f\x9e\xee\x99t\x02\x19" +
|
||||
"f\xbaz 0%\x97\x93\xc2\xf2\x88\x82\x06\xce+\x8e" +
|
||||
"\xb3.\x09\x06\xceD>\xd23p\xe5\x09Zvf\xde" +
|
||||
"\x84\xc9\xcdt\x0f\xdd=\x91 \xc8\x87 b\xf9\x05\x82" +
|
||||
"\"\xca\xc9ayW\xa0\xde\xc1\xa9\xe7\xb2%\xb5\xb2+" +
|
||||
"*\xa5\xa8X\xb0\x85\x8a\xb5\x8b\xc8\xeeJ\xc1\xba\"\xac" +
|
||||
"\xe5\xaeko=\xdd\xd3\x1f\x99\x84$\xc8\xfe\xb1\xffM" +
|
||||
"\x9e~\xde\xf7}>~\xcf\xef}\xde'\xd7wT6" +
|
||||
"r\xf5\xe1\x9a\x08\x80\xbc%\\a\xb1\xba\x0f\x97n\xbf" +
|
||||
"\xeag\xabAN Z\xf7\xbc\xd6\x1a\xff\xd6\\\xfd\x09" +
|
||||
"\x84y\x01`\xca\xe2\x8a\xa5(\xad\xad\x10\x00\xa4U\x15" +
|
||||
"\xbf\x06\xb4\xee\x1b\xb5\xfb\x99\xe7fl\xba\x17\xc4\x04\xef" +
|
||||
"+\x03NaB+J=\x02i\x16\x85u\xd2Q\xfa" +
|
||||
"e\xdd\"^\xb7 \xfe\xc1{\xa4\x1d\xdc:D[\xef" +
|
||||
"\x13\xeaP:d/8(\xd0\xd67\xe6\xdf\xdf\xf1\x0f" +
|
||||
"\x9b\xdfY\x03b\x82\xeb\xb5\xf5+\x95KQ:XI" +
|
||||
"\x9a\x07*\xe7\x02Z_o\x1a\xfd\xfc\x7f\xbe\xf7\xf6Z" +
|
||||
"\x10\xafF(Y\xfai\xe5\xc7\x08(}U\xf9\xbf\x80" +
|
||||
"\xd6\xa1\x0b\x0b\xce\xbf\xfc\xe6\x0d\xf7\x818\x81\x14\x90\x14" +
|
||||
"6T\x8d\xe3\x00\xa5\x9dU\x0d\x80\xd6\xe93\x7f\\w" +
|
||||
"\xf7\x849\x8f\x82<\x019\x800G\x1a\x07\xab\x12\xa4" +
|
||||
"q\xa2\x8a\xaci\x98yhob\xca\xe3\x9b\xcaL\xb7" +
|
||||
"\x15\xf7\x0f\xabC\xe9\xf002\xe8\xd0\xb0\xbb\x00\xad\xdf" +
|
||||
"\x8fx\xea\xbd\xe2M\xaf>^:\xcfV\xaa\x1f^G" +
|
||||
"\xbb\xb5\x0c'\x85q\xddW\xdd\xf9\xd3\x03/=\x01\xf2" +
|
||||
"DD\xebx\xfb5G\xf9m\xbb>\x81\xf9(\xd0\xf1" +
|
||||
"Sv\x0e\xdfA\xc6\xef\xb5u\xdf\xbf\xf6\xb5\x1f?\xfa" +
|
||||
"\xd2\xba\xa7@\xbe\x1a\x11\xc0\x0e\xd6\xd8\xea?\x90B}" +
|
||||
"5\x19\xbf\xe9\xd8\xbe9\xf9\x0d[w8\xee\xdb\xdf\xff" +
|
||||
"\xad\x9a\xe3 d\xadi\xf9&?\xff\xd9\xd4\xb3\xa5\xc0" +
|
||||
"\x84\xe9\xd3\xec\xeas\x088E\xa9\xaeE@\xeb\x86\x8f" +
|
||||
"O\xcd\x9d\xfd\x7f\x1d\xff\x1dX\xbb<\xb2\x94\xd6\xae\xeb" +
|
||||
"8\xb7?\x96\xcc?_\xe6\xb0\x1d\xbb\x9e\xc8.\x946" +
|
||||
"D\xc8\xe1\x87\"d\xc2\x8b\x7fsK\xd5\x92S3w" +
|
||||
"\x838\xd1\xdd\xe6\xc5H\x92\xb6\x09\xdd\xce\x7f\xafl\xf9" +
|
||||
"\xc9\xcb\xe5p\xb2c\xb23\xd2\x8e\xd2>\xdag\xca\xde" +
|
||||
"\x88m\xcf\x03\xfb\xb7^S\xf9\xcc\xd7\xaf\xf4\x17\xe6\x13" +
|
||||
"#\xdaQ\xba0\x82N\xfdj\x04Efd\x0b\x1e\x7f" +
|
||||
"\xbd>\xf4j0\xefr\xf44E\x86E)\xefc\xcf" +
|
||||
"N\x8f\xa8_\xae~\xbdl7[1\x1ckEiL" +
|
||||
"\x8cv\x1b\x19#\xe5\xd6\x05\x8fm\x0c\x9fz\xec-\xb2" +
|
||||
"4\x00\xb80\x01m\xca\x9e\x98\x8e\xd2\x81\x98\x9d\xedX" +
|
||||
"\x0d\x0fh%v\xff\xd3\xffL\xcf|\xf4N?\x96J" +
|
||||
"M\xf1s\xd2\xec8\xfdj\x89\x93\xa1''\xee\xb9\xfb" +
|
||||
"\x8b\x87\x0e\x1f)\x19j\xc7\xf0\xb9\xb8\x9d\xc2\xbdq\x8a" +
|
||||
"\x9f\x87\x80\xb2(\xd9\x9a\x1f\xc5\xbbP:ko\xf7\x85" +
|
||||
"\xad\xcd\x9dR\xc6\xac\xfc\xf9?\x1f\x0f$\xedl\xfc3" +
|
||||
"\x84\x905\xe7_\x17tU-?y2x\xd0\x89\xb8" +
|
||||
"\x1d\x91\x0b\xf6\xd2\xdf\xfe\xd7\xe9G\xce\xe43\xbf\xb2\x81" +
|
||||
"\xe7\xc6l\xe4\xc8i\x04\xcd\x89#\x09\xe85\xb5\x91\x19" +
|
||||
"\xe3\x8e\xb5\x9dvR\xe9lQ5j:)\\9\x8a" +
|
||||
"\xb6\xb8\xe1\xce&\xb6p\xea\xad\xa7\xfb\x94|\xd3\xa8i" +
|
||||
"(\xc9\xa3l\x90\x8dZ\x87\x12\xab\xa9\x01\xb0\xba\xff\x7f" +
|
||||
"\xc3\xad\xcf\xbf1\xe7\x9cS\x0b\xb6\xb1\xf3k&\x134" +
|
||||
"\x1e\xbe\xa7y\xee?\x8e\xdb\x7f.h\xec\xec\x1aB\xa7" +
|
||||
"\xa4\xd4\xd0I\x1dS\xcf\xfc\xcbU\x0f\xbfy\xae?\x08" +
|
||||
"\xae\xaa\xa9CiC\x8d\x0dAR\xfer\xe6\x7f\x1cI" +
|
||||
"D\x13\xe7\xcb\x02Xa'\xaf\xa6\x0b\xa5\x035v\xf2" +
|
||||
"j\xde\"\x98\xdd\xf7\xc9\x1dK>\xbc\xf7\xeb\x0b\xe5\xb9" +
|
||||
"\xb6\xb7~eL\x12\xa5\x83cl~\x19C\xc8xb" +
|
||||
"\xdeoV\x9c\xd9<\xea\x9b\xbe$\x97\xe8B\xa9'a" +
|
||||
"\x93\\b\x9dt\x94~Y\x1f\x08\xcf\xd67\xafx\xe7" +
|
||||
"\xdb@-\xecK\xb4\x92\xc3\x8f\x0bO\x9f\\\xf9\x8b;" +
|
||||
"\xbe\x0b:\xbc7\xf1\x199|(A\x0e/\xfb\xf2\xc9" +
|
||||
"\x9b\x1fY\xf8\xc2\xf7\xc1\xc4&V\xd3R\xb3\xa8\xaa," +
|
||||
"\xa7\x17B\xe9\xeb\xdc\x9f\xe9Ii\xa5\xa0\x16\xa65\x15" +
|
||||
"\xcdEL5\xb3i\xc5dI\xd6`\x144\xd5`m" +
|
||||
"\x88r\x8c\x0f\x01\x84\x10@T\xba\x00\xe4;y\x94s" +
|
||||
"\x1c\x8a\x88qJ\xbd\x98%\xe1\"\x1ee\x93C\x91\xe3" +
|
||||
"\xe2\xc4<\xe2\xe2q\x00r\x8eGy\x09\x87\xc8\xc7\x91" +
|
||||
"\x07\x10\x8b\x1b\x01\xe4%<\xcak8\xb4\x0aL\xcf+" +
|
||||
"*S!j\xce\xd0u\xac\x06\x0e\xab\x01-\x9d\x99z" +
|
||||
"\x8f\xd2\x9e\x83(\x0b\x88\x85\xae\xbbL\x8c\x00\x87\x11@" +
|
||||
"k\x91V\xd4\x8d\xf9\xaa\x89\xd9\\\x92u\xe8\xcc\xc0E" +
|
||||
"X\x01\x1cV\x00\x0e\xe4^\x8a\x19FVSg+\xaa" +
|
||||
"\xd2\xc9t\x00\xf2\xac\x92\x0f\x03x\xa4\x8d.\xbd\x8b\xf5" +
|
||||
"[\x81\x13'\x0a\xe830\xba\xf0\x13\xaf\xdc\x05\x9c8" +
|
||||
"V\xb0t\xd6\x995L\xa6\xe3\xfcL\xc1\xde\x9b\xd7\xd4" +
|
||||
"F\xb4\x8a\xaa\xf3\x01\x99\xee|\x88\xd2\xa9\x8d\xd8\x86\xbe" +
|
||||
"u|_\xebn\xcae\x99jF[\xd4\x0e\xad,\xe4" +
|
||||
"\xad\xfd\x85\xbc\xb5\x14\xf25\x81\x90\xaf\x9a\x0e /\xe3" +
|
||||
"Q\xbe\x9fC\x91/\xc5|m\x1d\x80\xbc\x92G\xf9A" +
|
||||
"\x0e\xad\xb4}HK\x06\x00\xbchv0\xc5,\xea\xcc" +
|
||||
" \xd9\x08\xc06\x1e\xed\xa0\x8f\x00\\\xd1\xcdt\xb2\xdd" +
|
||||
"MBT\xd1\xd3\x8b\xbcD\x0d\x10\xe9\x19K\xb2\x86\x99" +
|
||||
"U;\xe7\xd9\xf2\x866-\x97M\xf7\x90W\xd5\xb6\x9d" +
|
||||
"c\xa7\x01 \x8a#o\x03@N\x14\xa7\x034d;" +
|
||||
"UMgV&k\xa45Ue\xc0\xa7\xcd\x15\xedJ" +
|
||||
"NQ\xd3\xcc;\xa8\xa2\xefA\xce\x01)\xa6w3}" +
|
||||
"\x92\x12\x80\xef\xf86EW\xf8\xbc!W{q\x9cq" +
|
||||
"\x1b\x80\xdc\xcc\xa3\xdc\x16\x88\xe3l\x8a\xe3,\x1e\xe5[" +
|
||||
"\x03q\x9cOql\xe3Q^\xc8\xa1\xa5\xe9\xd9\xce\xac" +
|
||||
"z\x13\x03^\x0f\"\xd00U%\xcf(f\xa5x\xac" +
|
||||
"\xd0\x0afVS\x0d\x8c\xf9\xfc\x0f\x88\xb1@\xa4\x84\xc1" +
|
||||
"09\xc9\x85\x94\x8b(M\x1d\x9fdFQ\xc8\x99\x86" +
|
||||
"\x1c\xf2<\x89L\x03\x90+y\x94\xe3\x1c6\xe8\xcc(" +
|
||||
"\xe6L\x8c\xf9\xd7\xec_\xe2T7|\x01\x18&\xfb\x83" +
|
||||
"\xe1d\x009\xc3\xa3\\\xe0\x10K\xd1\xcbO\x0f\xb0\x01" +
|
||||
"\x8f\x0e\x0a\x17o\x05\x90M\x1e\xe5\x95\x1cZ\x86sH" +
|
||||
"\x0b`\xc6\x8dhm\xc60[\x0a\xee_+2\x86\xd9" +
|
||||
"\xa6\xe9&\x0a\xc0\xa1\x00\x84[\xcd`M\x1dTS-" +
|
||||
"\x99\x1c\xbb9\xcb\xab&\x86\x81\xc30\x0cXT\x0e>" +
|
||||
"\xa2DlN\xb5\xbb\xdeL 0\xfc\x1d\x8f\xf2\xdf\x07" +
|
||||
"\xbc\xa9'\x1e\xbb\x9eG\xf9F\x0e-%\x9d\xd6\x8a\xaa" +
|
||||
"9\x0fx\xa5\xb3\x0c\xf3)\x06\xd1\xb4\xce|8\x0c=" +
|
||||
"\xd4.9\x94\x05;\xaa+y#h^\xb2?\xf3(" +
|
||||
"\xb0\xd7\xf2(O\xed?\x86+\xf2\xcc0\x94N\xd6\xa7" +
|
||||
"B\xc3\xfd\xb0\x0dUY\x9a\x00\x9bd\x0e\xcfO\xd2\x99" +
|
||||
"!\x14s&YQmY\x8e\x19\x94\xde\xf1<\xca\xd7" +
|
||||
"s\x18\xc1\xef-\xc7\x8e\x89\x1b\xfd0\xd52]\xd7t" +
|
||||
"\x8c\xf9\xf7`\x09}\xe9\xd2\x01\xa8\xa9\xcd\xccT\xb29" +
|
||||
"\xa4\xca\xf0\x9a\xb22\x8c\x0eV\xda~\xd8\x1c\xf1\xf8\x06" +
|
||||
"\x02h\xbeWQ\x10\xc2b<\xcaWphu\xeaJ" +
|
||||
"\x9a\xb51\x1d\xb3Zf\x8e\xa2j)\x9e\xa5\xfb\xe0e" +
|
||||
"\xc4\xa5\x1e\x9a\xb4K\xcd\x00o\xd5\xc0\xebuV\x0aB" +
|
||||
"iy[\xadcs\xdc\xb3y\xf98\xff>\xf4\xd2\xbc" +
|
||||
"\xaa\xdd'l\x8f\x92\xd6\x13^\xef\xe7Q\xde\x14\xa0\xf6" +
|
||||
"\x0dD^\x8f\xf2(?\xcd\xa1\x18\x0a\xc51\x04 >" +
|
||||
"I(\xd9\xc4\xa3\xbc\x9d\xeb}k\xb2n\xa6\x9a\xcd\xd9" +
|
||||
"N\x10\x98\xe1K\xc9\xc4\xe6l'\x03\xde\xb8\\z\xab" +
|
||||
"\x1c$\x1eZ\xbb\xa1\xe5\x98\xc9\x9aY:\xa7\xe8\x8a\x99" +
|
||||
"\xedf\xce\xf7\x12\x18\xdd\xa4\x0e\x84\xdbd\x9f\xea!\xfc" +
|
||||
"F\xddF%\x00\x87q>G\x0a,\xd0_\x0c`\xad" +
|
||||
"\xb39Y\xa6\xa9}0\xe0WL\x09\x07h\x0ct\x05" +
|
||||
"\xfa\xeas\x0bfV\xd0T\x83\xec\x0b\xa4~Z\x7f\xa9" +
|
||||
"\xd7\xfd\xd4\xbbt\xba~u0\xf3%:\xdd\xb0\xd5O" +
|
||||
"\xb2\x18\xe2\x9c\xcco\xdb\x01 o\xe7Q~\x81\xc3\x06" +
|
||||
"\xe7\xa6\xc7\x98\xffR.e\xcb\xb9\xcffiP\x9bV" +
|
||||
"r>\xe5Z:+\xe4\x944\x9b\x81\xa5\xbb\x1b\x10\x81" +
|
||||
"C\xb4!\x92/\xe8\xcc00\xab\xa9rQ\xc9ey" +
|
||||
"\xb3\xc7\xeb\xb7\xd4b\xbeMg\xddY\xd4\x8aF\x93i" +
|
||||
"\xb2\xbcP0\x8d\xa1tc~\x80\x88\x1f\x84l\xce(" +
|
||||
"c\xe8:\x9f{\xbc\x00M\xec\xf2)0Z,f=" +
|
||||
"\xee\xb3rZ\xda\xce\x1bD\xe7(\xf9\xbe\x14X1h" +
|
||||
"\xad\xf6\xaat\x97\x91\xff\x9a\xba\x87\x81\x1bvr\xdd\xee" +
|
||||
"h\x03&S\x094\xf2(\xcf\x0a\x98\xdc29\xe0\x87" +
|
||||
"k\xf2\xecv\xdf\x0f\xe1\xdfY\x8fkU-\xcb\x13s" +
|
||||
"\xbb\xc1,9\xd3\x04\xc2-\xbe\xce@\xf6\x05\x0bjn" +
|
||||
"\xa1\xd6\xf6\x90l\x9c\xea\xda(\xf5`+@j\x09\xf2" +
|
||||
"\x98Z\x83\xbe\x99\xd2*\x9c\x0e\x90ZF\xf2\xfb\xd1\xb7" +
|
||||
"TZ\x8b\x09\x80\xd4J\x92?\x88\xde\xc3BZ\x8f\xbb" +
|
||||
"\x00R\x0f\x92x\x0b\xa9\x87x\xbb$\xa4\xcd\xf6\xf6\x9b" +
|
||||
"H\xbe\x9d\xe4\xe1P\x1c\xc3\x00\xd26\xac\x03Hm!" +
|
||||
"\xf9\xcb$\xaf\xe0\xe2X\x01 \xed\xc1.\x80\xd4n\x92" +
|
||||
"\xbfFr!\x1c\xa7\xb7\x95\xb4\x17u\x80\xd4\x8fH\xfe" +
|
||||
"\x06\xc9+G\xc7\xb1\x12@\xdao\xcb_'\xf9\xbb$" +
|
||||
"\xaf\x1a\x13\xc7*\x00\xe9 \xae\x06H\xbdM\xf2#$" +
|
||||
"\x1f\x86q\x1c\x06 \x1d\xc6\xad\x00\xa9#$\xff%\xc9" +
|
||||
"\x87W\xc4q8\x80\xf4\xa9m\xcf1\x92\x7fN\xf2\xea" +
|
||||
"P\x1c\xab\x01\xa4\x13\xb8\x03 \xf59\xc9\x7fG\xf2\x88" +
|
||||
"\x10\xc7\x08\x80t\xd6\xf6\xeb\x0c\xc9+\xb9\xb2\xbe\xdeE" +
|
||||
"TY\xf3\xcek\x86\x972V\xaaqt\xe0\xde\xa6E" +
|
||||
"\xa9A\xc7\xa8?)\x03\xc4(\xa0U\xd0\xb4\xdc\x9c\xde" +
|
||||
"H\x8d\x9aJ\xa7\xe1>\x14b\xfe\xf0\x02\x90\x84\xde\xbd" +
|
||||
"\x0fQMm\xc9xDP\xce:\xae%Y\xa3\xa9h" +
|
||||
"j\xc5\x02\xd4f\x14\x93e<\xce\xd1\x8b\xeaL]\xcb" +
|
||||
"\xcfC\xa6\xe7\xb3\xaa\x92\x1b\x84\x8d\xaa\x80\xc3*(Q" +
|
||||
"\x82\xbb\xf7\xc0\xd4t\xf1g\x8f\x87h\xae\x1c\xd1\xb5\x85" +
|
||||
"i\xf3\x94\xce\xa1\xf0\xd4d\xbf\x7f\x8b\xaa\x01B\xaa\xed" +
|
||||
"Vr\xc5\x1fBO\xbd[\x89d\x83\xd3\x8a\x0c\xf6(" +
|
||||
"pg\x19\x83SI\xef\x86\xb0\xf7\x85\x8a\x811#\x9d" +
|
||||
"\xc3\x95\xf6\x1f\xb2\xf9\x9d\xcct~\xd1\xeb\x96\xde\x16B" +
|
||||
"\xf0\x9a\xbf\xb4\xd5IfD\x87\xe2\xba?\xf3\x19\xfc=" +
|
||||
"\xd4\xcf\xc5\xdf\xcf\xb5\xef\xf6\x9c\x817\x11\xe5~!\x8f" +
|
||||
"\xf2\xa2@\xeeYk?o\xa2\xa4?\x0c\x11y\xae4" +
|
||||
"\x0d\xa1\x8b\xa2\xc0\xa3\xbc\x8c\xc3(=^1\xe6\x0f\x87" +
|
||||
"{\x19\xdd\xfb\xc1NPhQ3\x0cp\x89\x8b\xe6\xc0" +
|
||||
"\xf5\xe1\x8dI\x07\xef\xce\x86\xe6\xb6\xdb\xf5\x0e\x1apo" +
|
||||
"\xf4Xv\xf2E\xdfe\x0d\xce\xa1\x84\xb3\xd1\xf6\x1c\xc6" +
|
||||
"\x1d\xc3\xa2;\xd0\x13\xf7,\x05N\xdc)\xa0?\xaaD" +
|
||||
"w2)n\xd3\x81\x137\x0b\xc8y\x83mt\x07\xd8" +
|
||||
"\xe2\xfa\x07\x80\x13\xd7\x0a\xc8{sitGb\xf5=" +
|
||||
"\xc3\x108q\xb9\x80!o\xde\x8f\xee@M\\\xdc\x05" +
|
||||
"\x9c\x98\x150\xec\x8d\xbc\xd1\x9d\xb9\x8a\xb7\xaf\x06N\x9c" +
|
||||
"\xef\x0f~\xa0\xc1\xf1\xa3\x11-\x17\xa3Pk\xa3\xb4\xf7" +
|
||||
"\x18\xc8\xd1\x02hD\xcb\xed\x81\xf9\x8b5\xc1\xb6\x96;" +
|
||||
"\xc9\x80hZ1Y#5gN\xfdc\x89\x00\xa0\x11" +
|
||||
"\xe5\x10\x06\xe6\x89\x00\x97\xfb\x08M\xb2Z;\xcf?\xb4" +
|
||||
"er\xd7\xff@J\xe2\xfb\xb3\x9a\xce\xf1&b\x81}" +
|
||||
"\xa9\x0b\xac\xe6Q\x1e\xcd\x0d\xda\xf8\x85.\xe6\x85\x0b\xfe" +
|
||||
"(-\xa6\xfd\xff\xd6\xdb\xff05N\xef\xf2(\x1f\x0b" +
|
||||
"\x94\xf5Q\x12~\xc0\xa3|<\xd08}D\xb5~\x8c" +
|
||||
"G\xf9\xbc?\xe4\xfc\xea\x01\x00\xf9<\x8f\xc9@#\"" +
|
||||
"\xfe\x89\x14\xbf\xa3\xeb\xdanC\xd0iC\xc2\xb8\x11 " +
|
||||
"UI\xd7x\xdcnCBN\x1b\"b;@*F" +
|
||||
"\xf2+\x82m\xc8\x18\xbc\x0d 5\x9a\xe4\xe3\xb1\xf7\xbb" +
|
||||
"F(\xea~\xa3\x96\xd3:ge\xd5~\xef6w\xea" +
|
||||
"\x8a\xe6L%\x9b+\xea\x0c\xfc\xab\xb5D6\xcd\x81\xdb" +
|
||||
"\xde\x19\xc7:\x93\x97\x14\x810\x83\x867\x95\xb9\x84\x17" +
|
||||
"\xe5\x90n\x9e\x19\xba\xae\xa1^\xd6\xc4N\xf6\x9bX\xaf" +
|
||||
"\x87\xa5^\xfcf\x1e\xe5y\x94\x8aF'\x15r\xbb\xdf" +
|
||||
"v\xd7\xa6\x95\xa2\xc1\xfa\xf8\x00<\xd3\xbd)\x80\xb1H" +
|
||||
"+\xe62I\x06\x82\xa9\xf7\x94\x85`\xd0f6\xc5\xa2" +
|
||||
".s9\x13d\xf7\xbf!\xe8\xfe\xd3#0Av\xc7" +
|
||||
"\xf8\xe8\xfeo\xab\xef\x04\xd9\x8dA\x9f\x09\xb2\xf3\xc1\xc6" +
|
||||
"h\xef\x09\xf2e<_\x9dk,\xc0\x18\x974X\x1d" +
|
||||
"\xf2<\xd2\xfb\xf7oY\xa5W]\xee\x98\xc0\xbd\x90\xfe" +
|
||||
"\x1c\x00\x00\xff\xff\xa1\x1ap\xe9"
|
||||
|
||||
func init() {
|
||||
schemas.Register(schema_db8274f9144abc7e,
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
package tunnelstore
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CleanupParams struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewCleanupParams() *CleanupParams {
|
||||
return &CleanupParams{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
|
||||
cp.queryParams.Set("client_id", clientID.String())
|
||||
}
|
||||
|
||||
func (cp CleanupParams) encode() string {
|
||||
return cp.queryParams.Encode()
|
||||
}
|
||||
@@ -1,545 +0,0 @@
|
||||
package tunnelstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
"github.com/cloudflare/cloudflared/vnet"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 15 * time.Second
|
||||
jsonContentType = "application/json"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTunnelNameConflict = errors.New("tunnel with name already exists")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrBadRequest = errors.New("incorrect request parameters")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrAPINoSuccess = errors.New("API call failed")
|
||||
)
|
||||
|
||||
type Tunnel struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
Connections []Connection `json:"connections"`
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
ColoName string `json:"colo_name"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
IsPendingReconnect bool `json:"is_pending_reconnect"`
|
||||
OriginIP net.IP `json:"origin_ip"`
|
||||
OpenedAt time.Time `json:"opened_at"`
|
||||
}
|
||||
|
||||
type ActiveClient struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Features []string `json:"features"`
|
||||
Version string `json:"version"`
|
||||
Arch string `json:"arch"`
|
||||
RunAt time.Time `json:"run_at"`
|
||||
Connections []Connection `json:"conns"`
|
||||
}
|
||||
|
||||
type Change = string
|
||||
|
||||
const (
|
||||
ChangeNew = "new"
|
||||
ChangeUpdated = "updated"
|
||||
ChangeUnchanged = "unchanged"
|
||||
)
|
||||
|
||||
// Route represents a record type that can route to a tunnel
|
||||
type Route interface {
|
||||
json.Marshaler
|
||||
RecordType() string
|
||||
UnmarshalResult(body io.Reader) (RouteResult, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
type RouteResult interface {
|
||||
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
|
||||
SuccessSummary() string
|
||||
}
|
||||
|
||||
type DNSRoute struct {
|
||||
userHostname string
|
||||
overwriteExisting bool
|
||||
}
|
||||
|
||||
type DNSRouteResult struct {
|
||||
route *DNSRoute
|
||||
CName Change `json:"cname"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewDNSRoute(userHostname string, overwriteExisting bool) Route {
|
||||
return &DNSRoute{
|
||||
userHostname: userHostname,
|
||||
overwriteExisting: overwriteExisting,
|
||||
}
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
UserHostname string `json:"user_hostname"`
|
||||
OverwriteExisting bool `json:"overwrite_existing"`
|
||||
}{
|
||||
Type: dr.RecordType(),
|
||||
UserHostname: dr.userHostname,
|
||||
OverwriteExisting: dr.overwriteExisting,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
|
||||
var result DNSRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = dr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) RecordType() string {
|
||||
return "dns"
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) String() string {
|
||||
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
|
||||
}
|
||||
|
||||
func (res *DNSRouteResult) SuccessSummary() string {
|
||||
var msgFmt string
|
||||
switch res.CName {
|
||||
case ChangeNew:
|
||||
msgFmt = "Added CNAME %s which will route to this tunnel"
|
||||
case ChangeUpdated: // this is not currently returned by tunnelsore
|
||||
msgFmt = "%s updated to route to your tunnel"
|
||||
case ChangeUnchanged:
|
||||
msgFmt = "%s is already configured to route to your tunnel"
|
||||
}
|
||||
return fmt.Sprintf(msgFmt, res.hostname())
|
||||
}
|
||||
|
||||
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
|
||||
// requested name is returned instead (should not be the common path, it is just a fall-back).
|
||||
func (res *DNSRouteResult) hostname() string {
|
||||
if res.Name != "" {
|
||||
return res.Name
|
||||
}
|
||||
return res.route.userHostname
|
||||
}
|
||||
|
||||
type LBRoute struct {
|
||||
lbName string
|
||||
lbPool string
|
||||
}
|
||||
|
||||
type LBRouteResult struct {
|
||||
route *LBRoute
|
||||
LoadBalancer Change `json:"load_balancer"`
|
||||
Pool Change `json:"pool"`
|
||||
}
|
||||
|
||||
func NewLBRoute(lbName, lbPool string) Route {
|
||||
return &LBRoute{
|
||||
lbName: lbName,
|
||||
lbPool: lbPool,
|
||||
}
|
||||
}
|
||||
|
||||
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
LBName string `json:"lb_name"`
|
||||
LBPool string `json:"lb_pool"`
|
||||
}{
|
||||
Type: lr.RecordType(),
|
||||
LBName: lr.lbName,
|
||||
LBPool: lr.lbPool,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) RecordType() string {
|
||||
return "lb"
|
||||
}
|
||||
|
||||
func (lb *LBRoute) String() string {
|
||||
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) UnmarshalResult(body io.Reader) (RouteResult, error) {
|
||||
var result LBRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = lr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (res *LBRouteResult) SuccessSummary() string {
|
||||
var msg string
|
||||
switch res.LoadBalancer + "," + res.Pool {
|
||||
case "new,new":
|
||||
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
|
||||
case "new,updated":
|
||||
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
|
||||
case "new,unchanged":
|
||||
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
|
||||
case "updated,new":
|
||||
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
|
||||
case "updated,updated":
|
||||
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
|
||||
case "updated,unchanged":
|
||||
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
|
||||
case "unchanged,updated":
|
||||
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
|
||||
case "unchanged,unchanged":
|
||||
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
|
||||
case "unchanged,new":
|
||||
// this state is not possible
|
||||
fallthrough
|
||||
default:
|
||||
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
// Named Tunnels endpoints
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID) error
|
||||
ListTunnels(filter *Filter) ([]*Tunnel, error)
|
||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
||||
RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error)
|
||||
|
||||
// Teamnet endpoints
|
||||
ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error)
|
||||
AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error)
|
||||
DeleteRoute(params teamnet.DeleteRouteParams) error
|
||||
GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error)
|
||||
|
||||
// Virtual Networks endpoints
|
||||
CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error)
|
||||
ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error)
|
||||
DeleteVirtualNetwork(id uuid.UUID) error
|
||||
UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error
|
||||
}
|
||||
|
||||
type RESTClient struct {
|
||||
baseEndpoints *baseEndpoints
|
||||
authToken string
|
||||
userAgent string
|
||||
client http.Client
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
type baseEndpoints struct {
|
||||
accountLevel url.URL
|
||||
zoneLevel url.URL
|
||||
accountRoutes url.URL
|
||||
accountVnets url.URL
|
||||
}
|
||||
|
||||
var _ Client = (*RESTClient)(nil)
|
||||
|
||||
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
||||
}
|
||||
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
|
||||
}
|
||||
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
httpTransport := http.Transport{
|
||||
TLSHandshakeTimeout: defaultTimeout,
|
||||
ResponseHeaderTimeout: defaultTimeout,
|
||||
}
|
||||
http2.ConfigureTransport(&httpTransport)
|
||||
return &RESTClient{
|
||||
baseEndpoints: &baseEndpoints{
|
||||
accountLevel: *accountLevelEndpoint,
|
||||
zoneLevel: *zoneLevelEndpoint,
|
||||
accountRoutes: *accountRoutesEndpoint,
|
||||
accountVnets: *accountVnetsEndpoint,
|
||||
},
|
||||
authToken: authToken,
|
||||
userAgent: userAgent,
|
||||
client: http.Client{
|
||||
Transport: &httpTransport,
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type newTunnel struct {
|
||||
Name string `json:"name"`
|
||||
TunnelSecret []byte `json:"tunnel_secret"`
|
||||
}
|
||||
|
||||
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("tunnel name required")
|
||||
}
|
||||
if _, err := uuid.Parse(name); err == nil {
|
||||
return nil, errors.New("you cannot use UUIDs as tunnel names")
|
||||
}
|
||||
body := &newTunnel{
|
||||
Name: name,
|
||||
TunnelSecret: tunnelSecret,
|
||||
}
|
||||
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return unmarshalTunnel(resp.Body)
|
||||
case http.StatusConflict:
|
||||
return nil, ErrTunnelNameConflict
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("create tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return unmarshalTunnel(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("get tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("delete tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListTunnels(filter *Filter) ([]*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = filter.encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListTunnels(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list tunnels", resp)
|
||||
}
|
||||
|
||||
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
|
||||
var tunnels []*Tunnel
|
||||
err := parseResponse(body, &tunnels)
|
||||
return tunnels, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseConnectionsDetails(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list connection details", resp)
|
||||
}
|
||||
|
||||
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
|
||||
var clients []*ActiveClient
|
||||
err := parseResponse(reader, &clients)
|
||||
return clients, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = params.encode()
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("cleanup connections", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route Route) (RouteResult, error) {
|
||||
endpoint := r.baseEndpoints.zoneLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
|
||||
resp, err := r.sendRequest("PUT", endpoint, route)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return route.UnmarshalResult(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
if bodyBytes, err := json.Marshal(body); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to serialize json body")
|
||||
} else {
|
||||
bodyReader = bytes.NewBuffer(bodyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url.String(), bodyReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "can't create %s request", method)
|
||||
}
|
||||
req.Header.Set("User-Agent", r.userAgent)
|
||||
if bodyReader != nil {
|
||||
req.Header.Set("Content-Type", jsonContentType)
|
||||
}
|
||||
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
||||
req.Header.Add("Accept", "application/json;version=1")
|
||||
return r.client.Do(req)
|
||||
}
|
||||
|
||||
func parseResponse(reader io.Reader, data interface{}) error {
|
||||
// Schema for Tunnelstore responses in the v1 API.
|
||||
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
|
||||
var result response
|
||||
// First, parse the wrapper and check the API call succeeded
|
||||
if err := json.NewDecoder(reader).Decode(&result); err != nil {
|
||||
return errors.Wrap(err, "failed to decode response")
|
||||
}
|
||||
if err := result.checkErrors(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !result.Success {
|
||||
return ErrAPINoSuccess
|
||||
}
|
||||
// At this point we know the API call succeeded, so, parse out the inner
|
||||
// result into the datatype provided as a parameter.
|
||||
if err := json.Unmarshal(result.Result, &data); err != nil {
|
||||
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
||||
var tunnel Tunnel
|
||||
err := parseResponse(reader, &tunnel)
|
||||
return &tunnel, err
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []apiErr `json:"errors,omitempty"`
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
func (r *response) checkErrors() error {
|
||||
if len(r.Errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(r.Errors) == 1 {
|
||||
return r.Errors[0]
|
||||
}
|
||||
var messages string
|
||||
for _, e := range r.Errors {
|
||||
messages += fmt.Sprintf("%s; ", e)
|
||||
}
|
||||
return fmt.Errorf("API errors: %s", messages)
|
||||
}
|
||||
|
||||
type apiErr struct {
|
||||
Code json.Number `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (e apiErr) Error() string {
|
||||
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
|
||||
if resp.Header.Get("Content-Type") == "application/json" {
|
||||
var errorsResp response
|
||||
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
|
||||
if err := errorsResp.checkErrors(); err != nil {
|
||||
return errors.Errorf("Failed to %s: %s", op, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusBadRequest:
|
||||
return ErrBadRequest
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return ErrUnauthorized
|
||||
case http.StatusNotFound:
|
||||
return ErrNotFound
|
||||
}
|
||||
return errors.Errorf("API call to %s failed with status %d: %s", op,
|
||||
resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package tunnelstore
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
)
|
||||
|
||||
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
||||
func (r *RESTClient) ListRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListDetailedRoutes(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list routes", resp)
|
||||
}
|
||||
|
||||
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
||||
func (r *RESTClient) AddRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
|
||||
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
||||
if err != nil {
|
||||
return teamnet.Route{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseRoute(resp.Body)
|
||||
}
|
||||
|
||||
return teamnet.Route{}, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
|
||||
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
||||
func (r *RESTClient) DeleteRoute(params teamnet.DeleteRouteParams) error {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
_, err := parseRoute(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return r.statusCodeToError("delete route", resp)
|
||||
}
|
||||
|
||||
// GetByIP checks which route will proxy a given IP.
|
||||
func (r *RESTClient) GetByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return teamnet.DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseDetailedRoute(resp.Body)
|
||||
}
|
||||
|
||||
return teamnet.DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
||||
}
|
||||
|
||||
func parseListDetailedRoutes(body io.ReadCloser) ([]*teamnet.DetailedRoute, error) {
|
||||
var routes []*teamnet.DetailedRoute
|
||||
err := parseResponse(body, &routes)
|
||||
return routes, err
|
||||
}
|
||||
|
||||
func parseRoute(body io.ReadCloser) (teamnet.Route, error) {
|
||||
var route teamnet.Route
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
func parseDetailedRoute(body io.ReadCloser) (teamnet.DetailedRoute, error) {
|
||||
var route teamnet.DetailedRoute
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
// setVnetParam overwrites the URL's query parameters with a query param to scope the Route action to a certain
|
||||
// virtual network (if one is provided).
|
||||
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
|
||||
queryParams := url.Values{}
|
||||
if vnetID != nil {
|
||||
queryParams.Set("virtual_network_id", vnetID.String())
|
||||
}
|
||||
endpoint.RawQuery = queryParams.Encode()
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
package vnet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type NewVirtualNetwork struct {
|
||||
Name string `json:"name"`
|
||||
Comment string `json:"comment"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type VirtualNetwork struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Comment string `json:"comment"`
|
||||
Name string `json:"name"`
|
||||
IsDefault bool `json:"is_default_network"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type UpdateVirtualNetwork struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
IsDefault *bool `json:"is_default_network,omitempty"`
|
||||
}
|
||||
|
||||
func (virtualNetwork VirtualNetwork) TableString() string {
|
||||
deletedColumn := "-"
|
||||
if !virtualNetwork.DeletedAt.IsZero() {
|
||||
deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
virtualNetwork.ID,
|
||||
virtualNetwork.Name,
|
||||
strconv.FormatBool(virtualNetwork.IsDefault),
|
||||
virtualNetwork.Comment,
|
||||
virtualNetwork.CreatedAt.Format(time.RFC3339),
|
||||
deletedColumn,
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package watcher
|
||||
|
||||
Reference in New Issue
Block a user