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
18 changes: 18 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ jobs:
CGO_ENABLED=1 GOARCH=arm64 ./mage-bin -v go:build
mv ghostunnel ghostunnel-darwin-arm64
lipo -create -output ghostunnel-darwin-universal ghostunnel-darwin-amd64 ghostunnel-darwin-arm64
- name: Codesign binaries
env:
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
CODESIGN_CERTIFICATE: ${{ secrets.CODESIGN_CERTIFICATE }}
CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.CODESIGN_CERTIFICATE_PASSWORD }}
run: |
./mage-bin -v apple:codesign ghostunnel-darwin-amd64
./mage-bin -v apple:codesign ghostunnel-darwin-arm64
./mage-bin -v apple:codesign ghostunnel-darwin-universal
- name: Notarize binaries
env:
NOTARIZE_ISSUER_ID: ${{ secrets.NOTARIZE_ISSUER_ID }}
NOTARIZE_KEY_ID: ${{ secrets.NOTARIZE_KEY_ID }}
NOTARIZE_KEY: ${{ secrets.NOTARIZE_KEY }}
run: |
./mage-bin -v apple:notarize ghostunnel-darwin-amd64
./mage-bin -v apple:notarize ghostunnel-darwin-arm64
./mage-bin -v apple:notarize ghostunnel-darwin-universal
- name: Upload artifact
uses: actions/upload-artifact@v6
with:
Expand Down
3 changes: 1 addition & 2 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ linters:
- std-error-handling
- common-false-positives
rules:
# Additional exclusions for os.Remove() in tests - cleanup that's best-effort
# Ignore errcheck in test files - test code often has best-effort operations
- linters:
- errcheck
text: "os.Remove"
path: "_test\\.go"
settings:
errcheck:
Expand Down
96 changes: 96 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# AGENTS.md

This file provides guidance to agents when working with code in this repository.

## Project Overview

Ghostunnel is a TLS proxy with mutual authentication support for securing non-TLS backend applications. It operates in two modes:
- **Server mode**: Accepts TLS connections and proxies to insecure backends (TCP or UNIX sockets)
- **Client mode**: Accepts insecure connections and proxies to TLS-secured services

## Build Commands

This project uses [mage](https://magefile.org) as the build system:

```bash
# Build the binary
mage go:build

# Run all tests (unit + integration)
mage test:all

# Run only unit tests
mage test:unit

# Run only integration tests (requires Python 3.5+)
mage test:integration

# Run tests in Docker (includes PKCS#11 tests with SoftHSM)
mage test:docker

# Generate test certificates for development
mage test:keys

# View coverage
go tool cover -html coverage/all.profile

# Build Docker images
mage docker:build

# List all available targets
mage -l
```

### Running a Single Test

For unit tests:
```bash
go test -v -run TestName ./...
go test -v ./auth/... # Run all tests in a package
```

For integration tests (Python):
```bash
cd tests && python3 test-name.py
```

### Linting

```bash
golangci-lint run
```

The project uses golangci-lint with configuration in `.golangci.yml`. Standard linters are enabled with exclusions for common error handling patterns.

## Architecture

### Package Structure

- **main** (`main.go`, `doc.go`): Entry point, CLI flag parsing (using kingpin), mode dispatch (server/client)
- **auth**: Authorization via X.509 certificate validation (CN, OU, DNS SAN, URI SAN, IP SAN checks)
- **certloader**: Certificate loading abstractions supporting PEM files, PKCS#12 keystores, PKCS#11 HSMs, SPIFFE Workload API, ACME, and macOS/Windows keychain
- **proxy**: Connection forwarding with configurable timeouts, connection limits, and PROXY protocol support
- **policy**: Open Policy Agent (OPA) integration for declarative access control policies
- **socket**: Network socket utilities including systemd/launchd socket activation
- **wildcard**: Pattern matching for URI-based access control
- **certstore**: Platform-specific keychain integration (macOS/Windows)

### Key Design Patterns

1. **TLSConfigSource interface**: Abstracts certificate sources (files, SPIFFE, ACME, keychain) behind a common interface for hot-reloading
2. **Conditional compilation**: Platform-specific features (PKCS#11, keychain, Landlock) use build tags (`pkcs11_enabled.go`/`pkcs11_disabled.go`)
3. **Signal handling**: SIGHUP triggers certificate reload; SIGTERM/SIGINT trigger graceful shutdown

### Testing

- Unit tests: Go standard testing in `*_test.go` files
- Integration tests: Python scripts in `tests/` directory using `tests/common.py` helper module
- Test certificates are generated in `test-keys/` via `mage test:keys`

## Key Flags

Server mode requires access control: `--allow-all`, `--allow-cn`, `--allow-ou`, `--allow-dns`, `--allow-uri`, `--allow-policy`, or `--disable-authentication`

Certificate sources are mutually exclusive: `--keystore`, `--cert/--key`, `--keychain-identity`, `--use-workload-api`, `--auto-acme-cert`

Safe addresses (localhost, 127.0.0.1, [::1], unix:, systemd:, launchd:) don't require `--unsafe-target` or `--unsafe-listen`
129 changes: 129 additions & 0 deletions certloader/acmetlsconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*-
* Copyright 2025 Ghostunnel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package certloader

import (
"crypto/tls"
"testing"

"github.com/caddyserver/certmagic"
"github.com/mholt/acmez"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// Note: Full ACME testing requires external ACME server interaction.
// These tests cover the code paths that can be tested without external dependencies.

func TestACMETLSConfigSourceGetClientConfigError(t *testing.T) {
// GetClientConfig should always fail for ACME sources
// (ACME is server-only feature)
source := &acmeTLSConfigSource{
magicConfig: certmagic.NewDefault(),
gtACMEConfig: &ACMEConfig{},
}

_, err := source.GetClientConfig(nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not supported in client mode")
}

func TestACMETLSConfigSourceCanServe(t *testing.T) {
source := &acmeTLSConfigSource{
magicConfig: certmagic.NewDefault(),
gtACMEConfig: &ACMEConfig{},
}

// CanServe should always return true (certmagic manages validity)
assert.True(t, source.CanServe())
}

func TestACMETLSConfigSourceReload(t *testing.T) {
source := &acmeTLSConfigSource{
magicConfig: certmagic.NewDefault(),
gtACMEConfig: &ACMEConfig{},
}

// Reload should be a no-op (certmagic auto-refreshes)
err := source.Reload()
assert.NoError(t, err)
}

func TestACMETLSConfigSourceGetServerConfigNilBase(t *testing.T) {
source := &acmeTLSConfigSource{
magicConfig: certmagic.NewDefault(),
gtACMEConfig: &ACMEConfig{},
}

// GetServerConfig should work with nil base config
config, err := source.GetServerConfig(nil)
require.NoError(t, err)
require.NotNil(t, config)

tlsConfig := config.GetServerConfig()
require.NotNil(t, tlsConfig)
assert.NotNil(t, tlsConfig.GetCertificate, "GetCertificate should be set")
assert.Contains(t, tlsConfig.NextProtos, acmez.ACMETLS1Protocol, "ACME-TLS protocol should be in NextProtos")
}

func TestACMETLSConfigSourceGetServerConfigWithBase(t *testing.T) {
source := &acmeTLSConfigSource{
magicConfig: certmagic.NewDefault(),
gtACMEConfig: &ACMEConfig{},
}

// GetServerConfig should preserve base config settings
base := &tls.Config{
MinVersion: tls.VersionTLS13,
NextProtos: []string{"h2", "http/1.1"},
}

config, err := source.GetServerConfig(base)
require.NoError(t, err)
require.NotNil(t, config)

tlsConfig := config.GetServerConfig()
require.NotNil(t, tlsConfig)
assert.Equal(t, uint16(tls.VersionTLS13), tlsConfig.MinVersion, "MinVersion should be preserved from base")
assert.Contains(t, tlsConfig.NextProtos, "h2", "base NextProtos should be preserved")
assert.Contains(t, tlsConfig.NextProtos, "http/1.1", "base NextProtos should be preserved")
assert.Contains(t, tlsConfig.NextProtos, acmez.ACMETLS1Protocol, "ACME-TLS protocol should be added")
}

func TestACMETLSConfigGetServerConfig(t *testing.T) {
magicConfig := certmagic.NewDefault()
base := &tls.Config{
MinVersion: tls.VersionTLS12,
}

acmeConfig := &acmeTLSConfig{
magicConfig: magicConfig,
base: base,
}

tlsConfig := acmeConfig.GetServerConfig()
require.NotNil(t, tlsConfig)

// Verify it's a clone (not the same pointer)
assert.NotSame(t, base, tlsConfig, "GetServerConfig should return a clone")

// Verify GetCertificate is set
assert.NotNil(t, tlsConfig.GetCertificate)

// Verify ACME-TLS protocol is added
assert.Contains(t, tlsConfig.NextProtos, acmez.ACMETLS1Protocol)
}
Loading
Loading