Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Architecture

## Overview

OutSystems Cloud Connector (`outsystemscc`) is a CLI tool that establishes secure reverse tunnels between private network endpoints and OutSystems Developer Cloud (ODC) Private Gateways. It enables ODC applications to reach services (REST APIs, SMTP, databases, etc.) that are not exposed to the public internet.

The tool is a thin wrapper around a forked version of [chisel](https://github.com/jpillora/chisel) (`github.com/outsystems/chisel`), adding OutSystems-specific connection logic: server URL resolution (with redirect handling), remote validation, query parameter generation, and token-based authentication via HTTP headers.

## System Context

```mermaid
graph LR
subgraph Customer Private Network
CC[outsystemscc]
EP1[Private Endpoint<br/>e.g. REST API]
EP2[Private Endpoint<br/>e.g. SMTP Server]
end

subgraph OutSystems Developer Cloud
PG[Private Gateway<br/>per stage]
APP[ODC Applications]
end

CC -- "reverse tunnel<br/>WebSocket over TLS" --> PG
CC -. "TCP/UDP forwarding" .-> EP1
CC -. "TCP/UDP forwarding" .-> EP2
APP -- "secure-gateway:port" --> PG
PG -- "tunneled traffic" --> CC

PORTAL[ODC Portal] -. "provides Token + Address" .-> CC
```

Traffic flow:
1. An operator activates a Private Gateway for a stage in the ODC Portal, obtaining a **Token** and **Address**.
2. `outsystemscc` uses these to establish a reverse tunnel (WebSocket over HTTPS, secured with SSH/ECDSA) to the Private Gateway.
3. ODC applications address the private endpoints as `secure-gateway:<port>`. Traffic is routed through the Private Gateway, across the tunnel, to the target endpoint in the customer's network.

## Code Structure

The entire application lives in a single Go package (`main`) with two source files:

```
cloud-connector/
main.go -- CLI entry point and all application logic
main_test.go -- Unit tests for remote validation and URL fetching
Dockerfile -- Alpine-based container image
.goreleaser.yaml -- Build and release configuration (Linux binaries + Docker)
go.mod / go.sum -- Go module definition
```

### main.go -- Component Breakdown

| Function | Responsibility |
|---|---|
| `main` / `client` | CLI flag parsing, orchestration of the connection lifecycle |
| `fetchURL` | HTTP GET to the server address; follows 302 redirects to resolve the actual tunnel endpoint |
| `validateRemotes` | Parses and validates remote definitions (`R:local:host:remote`); rejects duplicate local ports |
| `generateQueryParameters` | Builds query string with a random session ID and declared local ports |
| `createHTTPClient` | Creates a `resty` HTTP client, optionally configured with a proxy |
| `generatePidFile` | Writes the process PID to `outsystemscc.pid` for process management |
| `headerFlags` | Custom `flag.Value` implementation for `--header` key:value pairs |

### Connection Lifecycle

```mermaid
sequenceDiagram
participant User
participant CC as outsystemscc
participant Server as Private Gateway

User->>CC: Run with Token, Address, Remotes
CC->>CC: Parse flags, validate remotes
CC->>Server: HTTP GET Address (resolve redirects)
Server-->>CC: Final URL (possibly via 302)
CC->>CC: Append query params (id, ports)
CC->>Server: Chisel client connects (WebSocket + SSH)
Server-->>CC: Connected
Note over CC,Server: Reverse tunnel active<br/>Keepalive every 25s (default)
CC->>CC: Wait for interrupt signal or error
```

## Key Dependencies

| Dependency | Purpose |
|---|---|
| `github.com/outsystems/chisel` (fork of `jpillora/chisel` v1.11.3-os.1) | Core tunneling engine -- WebSocket transport, SSH encryption, reverse port forwarding |
| `github.com/go-resty/resty/v2` | HTTP client for server URL resolution and redirect handling |
| `github.com/gorilla/websocket` | WebSocket protocol (transitive, via chisel) |
| `golang.org/x/crypto` | SSH cryptographic primitives (transitive, via chisel) |

The `replace` directive in `go.mod` pins chisel to the OutSystems fork, which contains platform-specific modifications.

## Build and Release

**GoReleaser** (`.goreleaser.yaml`) produces:
- Linux binaries for `386`, `amd64`, and `arm64`
- A Docker image published to `ghcr.io/outsystems/outsystemscc`

The Docker image is a minimal Alpine container with the static binary copied to `/app/outsystemscc`.

**Dependabot** runs monthly to keep Go module dependencies up to date.

## Network and Security Model

- **Outbound-only**: `outsystemscc` initiates all connections. No inbound ports need to be opened in the customer's firewall.
- **Transport encryption**: HTTP transport upgraded to WebSocket, encrypted with SSH (ECDSA + SHA256). The outer connection uses TLS (port 443) with a valid X.509 certificate.
- **Authentication**: A token (issued by the ODC Portal) is passed as an HTTP header on the tunnel connection.
- **Proxy support**: HTTP CONNECT and SOCKS5 proxies are supported for environments where direct outbound access is restricted.
- **Keepalive**: Configurable interval (default 25s) to prevent intermediate proxies from closing idle connections.
- **Retry**: Automatic reconnection with configurable max retry count and interval.

## Architectural Principles

1. **Single binary, zero runtime dependencies** -- The tool is a statically compiled Go binary. No interpreters, shared libraries, or configuration files are required.
2. **Outbound-only connectivity** -- The tunnel is initiated from the private network side, avoiding the need for inbound firewall rules.
3. **Thin wrapper over proven tunneling** -- Application logic is minimal. The heavy lifting (WebSocket transport, SSH encryption, port forwarding) is delegated to chisel.
4. **Platform as Linux, distribute as container** -- Binaries target Linux only. Windows users run via WSL or Docker.
80 changes: 80 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# CLAUDE.md

## Project Overview

OutSystems Cloud Connector (`outsystemscc`) is a Go CLI tool that creates reverse tunnels between private network endpoints and OutSystems Developer Cloud (ODC) Private Gateways. See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full system design and component breakdown.

## Quick Reference

```bash
# Build
go build -o outsystemscc .

# Test
go test ./...

# Lint / format
go vet ./...
gofmt -s -w .

# Snapshot release build (local)
goreleaser --clean --snapshot --config .goreleaser.yaml
```

See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full development workflow, PR process, and release details.

## Directory Structure

```
cloud-connector/
main.go -- All application logic: CLI parsing, URL resolution, remote
validation, query parameter generation, chisel client setup
main_test.go -- Unit tests (uses httpmock, no external services needed)
go.mod / go.sum -- Go module; note the `replace` directive pinning chisel
to the OutSystems fork (github.com/outsystems/chisel)
Dockerfile -- Minimal Alpine image with static binary at /app/outsystemscc
.goreleaser.yaml -- Release config: Linux binaries (386/amd64/arm64) + Docker image
.github/CODEOWNERS -- PR reviewers: global-routing-and-security, cloud-enablement-services
.github/dependabot.yml -- Monthly Go module dependency updates
FAQ.md -- Deployment examples (Azure Container Instances, network setup)
README.md -- User-facing documentation, usage examples, firewall setup
images/ -- Assets for README
dist/ -- Build artifacts (gitignored)
```

## Key Patterns and Conventions

- **Single `main` package**: The entire application is two files (`main.go` and `main_test.go`). Do not introduce additional packages without strong justification.
- **Thin wrapper over chisel**: Application logic is intentionally minimal. The tunneling engine (WebSocket transport, SSH encryption, port forwarding) is delegated to the chisel fork. Avoid duplicating functionality that chisel already provides.
- **`replace` directive in go.mod**: The import path uses `github.com/jpillora/chisel` but the actual code comes from `github.com/outsystems/chisel`. This is intentional -- do not remove the `replace` directive.
- **Version injection**: The `version` variable in `main.go` is set at build time by GoReleaser via `-ldflags`. The default value `"dev"` is used during local development.
- **HTTP redirect handling**: `fetchURL` uses a no-redirect policy and manually follows 302s to resolve the final tunnel endpoint URL. This is deliberate, not accidental.
- **Test mocking**: Tests use `httpmock` with `ActivateNonDefault` on the resty client. Follow this pattern for any new HTTP tests.

## Domain Terminology

| Term | Meaning |
|---|---|
| **Private Gateway** | ODC-side endpoint that accepts reverse tunnel connections, one per stage |
| **Remote** | A port-forwarding rule in the format `R:<local-port>:<remote-host>:<remote-port>` |
| **Token** | Authentication credential issued by the ODC Portal, passed as an HTTP header |
| **Address** | Server URL from the ODC Portal; may return a 302 redirect to the actual endpoint |
| **Session ID** | Random 9-digit integer appended as a query parameter to identify the connection |
| **Local port** | The port on the Private Gateway side (not the client side) that maps to the remote endpoint |

## Common Pitfalls

- **Linux-only binaries**: GoReleaser is configured to build only for Linux (`goos: linux`). Windows/macOS users run via WSL or Docker. Do not add other OS targets without product decision.
- **Duplicate local ports are rejected**: `validateRemotes` enforces unique local ports across all remote definitions. Two remotes cannot share the same local port even if they point to different hosts.
- **No inbound firewall rules**: The tool initiates all connections outbound. Never introduce logic that listens on a port or requires inbound connectivity.
- **Proxy passthrough**: The `--proxy` flag configures both the resty HTTP client (for URL resolution) and the chisel client (for the tunnel). Both must go through the same proxy.
- **`--hostname` is deprecated**: The flag still exists for backward compatibility but is ignored. It prints a deprecation warning. Do not remove it without a major version bump.
- **PID file path**: `generatePidFile` writes to the current working directory, not a configurable path. This is intentional for container environments.
- **No linter config checked in**: `.editorconfig` and `.markdownlint.json` are gitignored. Use `go vet` and `gofmt` as the baseline.

## Related Documentation

- [ARCHITECTURE.md](./ARCHITECTURE.md) -- System context, component breakdown, connection lifecycle, security model
- [CONTRIBUTING.md](./CONTRIBUTING.md) -- Prerequisites, build/test commands, code style, branch naming, PR process, releases
- [FAQ.md](./FAQ.md) -- Deployment examples for Azure Container Instances
- [README.md](./README.md) -- User-facing usage guide, firewall requirements, examples
80 changes: 80 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Contributing to OutSystems Cloud Connector

## Prerequisites

- **Go 1.25.1+** -- see [go.dev/dl](https://go.dev/dl/) for installation
- **Git**
- **Docker** (optional, for building container images)
- **GoReleaser** (optional, for testing release builds locally)

## Getting Started

Clone the repository and download dependencies:

```bash
git clone https://github.com/OutSystems/cloud-connector.git
cd cloud-connector
go mod download
```

## Building

Build the binary:

```bash
go build -o outsystemscc .
```

Test a release build locally with GoReleaser (produces Linux binaries and Docker images):

```bash
goreleaser --clean --snapshot --config .goreleaser.yaml
```

Build artifacts land in `dist/`.

## Running Tests

```bash
go test ./...
```

Tests use [httpmock](https://github.com/jarcoal/httpmock) for HTTP mocking. No external services or credentials are needed.

## Code Style

There is no project-level linter configuration checked in. Follow standard Go conventions:

- Run `go vet ./...` before submitting.
- Run `gofmt -s -w .` to format code.
- Keep imports grouped: stdlib, then external packages (the standard `goimports` ordering).
- The project is a single `main` package -- keep it that way unless there is a strong reason to refactor.

## Branch Naming and PR Process

Branch names follow the pattern `<JIRA-ID>` or `feature/<JIRA-ID>` (e.g., `RDODCP-283`, `feature/RDGRS-811`). Use descriptive names when there is no associated ticket.

### Workflow

1. Create a branch off `main`.
2. Make your changes and ensure tests pass (`go test ./...`).
3. Push and open a pull request targeting `main`.
4. PRs require review from the code owners defined in [`.github/CODEOWNERS`](.github/CODEOWNERS): **@OutSystems/global-routing-and-security** and **@OutSystems/cloud-enablement-services**.
5. After approval, merge via the GitHub UI.

## Releases

Releases are managed with [GoReleaser](https://goreleaser.com/) and follow semantic versioning (tags like `v2.0.3`). The release pipeline:

1. Runs `go mod tidy` and `go generate ./...` as pre-build hooks.
2. Cross-compiles Linux binaries for `amd64`, `arm64`, and `386`.
3. Builds and pushes a Docker image to `ghcr.io/outsystems/outsystemscc`.
4. Generates a changelog (excluding `docs:` and `test:` prefixed commits).

## Dependency Management

Go modules (`go.mod` / `go.sum`) manage dependencies. Dependabot is configured to open monthly update PRs for Go modules. The project uses a forked version of chisel (`github.com/outsystems/chisel`) via a `replace` directive in `go.mod`.

## License

By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE).