Skip to content
Merged
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
23 changes: 23 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference

version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "[dep][actions]"
include: "scope"

- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "[dep][go]"
include: "scope"
groups:
golang-dependencies:
patterns:
- "*"
35 changes: 3 additions & 32 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,32 +1,3 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum

# env file
.env

# Editor/IDE
# .idea/
# .vscode/
.DS_Store
.vscode/
dist/
45 changes: 45 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
version: 2

before:
hooks:
- go mod tidy

builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
goarch:
- amd64
- arm64
ldflags:
'-s -w -X github.com/sbldevnet/{{ .ProjectName }}/cmd.version={{.Version}} -X github.com/sbldevnet/{{ .ProjectName }}/cmd.commit={{.Commit}}'

universal_binaries:
- replace: true

snapshot:
version_template: "{{ .Version }}-SNAPSHOT"

archives:
- name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
formats:
- zip
format_overrides:
- goos: linux
formats:
- tar.gz

changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"

checksum:
name_template: 'checksums.txt'

release:
draft: true
105 changes: 105 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# Cloudflared Proxy

![logo](./img/logo.png#gh-light-mode-only)
![logo](./img/logo_dark.png#gh-dark-mode-only)

A flexible reverse proxy for Cloudflare Access applications.

This tool allows you to proxy multiple Cloudflare Access protected applications to your local machine, with easy configuration via command-line flags, a configuration file, or environment variables.

## Features

- **Multiple Endpoints**: Proxy multiple applications simultaneously.
- **Flexible Configuration**: Use command-line flags, a configuration file (YAML, JSON, etc.), or environment variables.
- **TLS Configuration**: Option to skip TLS verification for non trusted certificates.

## Installation

The binary can be downloaded from the [GitHub Releases](https://github.com/sbldevnet/cloudflared-proxy/releases) page.

Alternatively, you can build from source:

```bash
go build -o cfproxy .
```

## Usage

The primary command is `run`, which starts the reverse proxies.

```bash
./cfproxy run [flags]
```

### Command-Line Flags

You can specify endpoints directly on the command line.

**Endpoint Format:** `[LOCAL_PORT:]HOSTNAME[:DEST_PORT]`

- `LOCAL_PORT`: (Optional) The port on your local machine (default: `8888`).
- `HOSTNAME`: (Required) The destination hostname.
- `DEST_PORT`: (Optional) The destination port (default: `443`).

**Examples:**

```bash
# Proxy example.com to localhost:8888
./cfproxy run -e example.com

# Proxy example.com to localhost:9000
./cfproxy run -e 9000:example.com

# Proxy example.com:8443 to localhost:8888
./cfproxy run -e example.com:8443

# Proxy example.com:8443 to localhost:9000
./cfproxy run -e 9000:example.com:8443

# Proxy multiple endpoints
./cfproxy run -e example1.com,9001:example2.com
# or
./cfproxy run -e example1.com -e 9001:example2.com

# Skip TLS verification
./cfproxy run -e example.com --skip-tls
```

### Configuration File

For a more persistent setup, you can use a configuration file. By default, `cfproxy` looks for a `config` file in `$HOME/.config/cloudflared-proxy/`. You can specify a different file with the `--config` or `-c` flag.

**Example `config.yaml`:**

```yaml
proxies:
- hostname: "app1.your-domain.com"
localPort: 8080
- hostname: "app2.your-domain.com"
localPort: 8081
destinationPort: 8443
- hostname: "app3.your-domain.com"
skipTLS: true
```

With a configuration file, you can start the proxies with a simple command:

```bash
./cfproxy run
```

Or with a custom config file path:
```bash
./cfproxy run -c /path/to/your/config.yaml
```

### Configuration Precedence

Configuration is loaded in the following order, with later sources overriding earlier ones:

1. **Configuration File**
3. **Command-Line Flags**

---

For more details on Cloudflare Tunnels, see the [official documentation](https://developers.cloudflare.com/cloudflare-one/tutorials/cli/).
121 changes: 121 additions & 0 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package cmd

import (
"fmt"
"os"
"path/filepath"

"github.com/sbldevnet/cloudflared-proxy/internal"
"github.com/sbldevnet/cloudflared-proxy/internal/config"
"github.com/sbldevnet/cloudflared-proxy/pkg/logger"

"github.com/spf13/cobra"
"github.com/spf13/viper"
)

func Execute() *cobra.Command {

cmd := &cobra.Command{
Use: "cfproxy",
Short: "A reverse proxy tool for Cloudflare Access applications.",
}

cmd.AddCommand(Run())
cmd.AddCommand(Version())

return cmd
}

func Run() *cobra.Command {
var (
endpoints []string
skipTLS bool
cfgFile string
)

cmd := &cobra.Command{
Use: "run",
Short: "Start reverse proxies",
Long: "Start reverse proxies to Cloudflare Access applications",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return initConfig(cfgFile)
},
RunE: func(cmd *cobra.Command, args []string) error {
if len(endpoints) == 0 && !viper.IsSet("proxies") {
cmd.Help()
return nil
}

if len(endpoints) > 0 && viper.IsSet("proxies") {
return fmt.Errorf("cannot specify endpoints via flags when a config file is used")
}

var proxyConfigs []config.ProxyConfig

if len(endpoints) > 0 {
proxyConfigs = make([]config.ProxyConfig, len(endpoints))
for i, endpoint := range endpoints {
proxy, err := config.ParseEndpointString(endpoint)
if err != nil {
return err
}
proxy.SkipTLS = skipTLS
proxyConfigs[i] = *proxy
}
} else {
var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
return fmt.Errorf("unable to decode into struct, %v", err)
}
proxyConfigs = cfg.Proxies
config.SetDefaults(proxyConfigs)
}

logger.Debug("cmd.Run", "Starting %d proxies", len(proxyConfigs))
logger.Debug("cmd.Run", "Proxy configs: %v", proxyConfigs)

err := internal.ProxyCFAccess(cmd.Context(), proxyConfigs, internal.NewLiveProxyService())
if err != nil {
return err
}

return nil
},
}

cmd.Flags().StringVarP(&cfgFile, "config", "c", "", "config file (default is $HOME/.config/cloudflared-proxy/config.yaml)")
cmd.Flags().StringSliceVarP(&endpoints, "endpoints", "e", []string{}, "List of endpoints to proxy in format [LOCAL_PORT:]HOSTNAME[:DEST_PORT]")
cmd.Flags().BoolVarP(&skipTLS, "skip-tls", "s", false, "Skip TLS verification")

return cmd
}

func initConfig(cfgFile string) error {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
if err != nil {
return err
}
viper.AddConfigPath(filepath.Join(home, ".config", "cloudflared-proxy"))
viper.SetConfigName("config")
}

if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; ignore error if not explicitly provided
if cfgFile != "" {
return err
}
} else {
return err
}
}

if cfgFile != "" {
logger.Debug("cmd.initConfig", "Config file path: %s", viper.ConfigFileUsed())
}

return nil
}
24 changes: 24 additions & 0 deletions cmd/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

var (
version = "dev"
commit = ""
)

func Version() *cobra.Command {
cmd := &cobra.Command{
Use: "version",
Short: "Print the version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Version: %s %s\n", version, commit)
},
}

return cmd
}
13 changes: 13 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Cloudflared Proxy Configuration Example
# Viper supports multiple formats: YAML, JSON, TOML, HCL, INI, envfile, and Java properties
# Copy this file to ~/.config/cloudflared-proxy/config.yaml and modify as needed

proxies:
# Destination hostname to proxy (required)
- hostname: "example.your-domain.com"
# Local port to proxy (optional, defaults to 8888)
localPort: 8888
# Destination port to proxy (optional, defaults to 443)
destinationPort: 443
# Skip TLS verification (optional, defaults to false)
skipTLS: false
Loading