Skip to content

Commit

Permalink
feat(caddy): add Caddy support for Outline services (#198)
Browse files Browse the repository at this point in the history
* Create a new config format so we can expand listener configuration for proxy protocol.

* Remove unused `fakeAddr`.

* Split `startPort` up between TCP and UDP.

* Use listeners to configure TCP and/or UDP services as needed.

* Remove commented out line.

* Use `ElementsMatch` to compare the services irrespective of element ordering.

* Do not ignore the `keys` field if `services` is used as well.

* Add some more tests for failure scenarios and empty files.

* Remove unused `GetPort()`.

* Move `ResolveAddr` to config.go.

* Remove use of `net.Addr` type.

* Pull listener creation into its own function.

* Move listener validation/creation to `config.go`.

* Use a custom type for listener type.

* Fix accept handler.

* Add doc comment.

* Fix tests still supplying the port.

* Move old config parsing to `loadConfig`.

* Lowercase `readConfig`.

* Use `Config` suffix for config types.

* Remove the IP version specifiers from the `newListener` config handling.

* refactor: remove use of port in proving metric

* Fix tests.

* Add a TODO comment to allow short-form direct listener config.

* Make legacy key config name consistent with type.

* Move config validation out of the `loadConfig` function.

* Remove unused port from bad merge.

* Add comment describing keys.

* Move validation of listeners to config's `Validate()` function.

* Introduce a `NetworkAdd` to centralize parsing and creation of listeners.

* Use `net.ListenConfig` to listen.

* Simplify how we create new listeners.

This does not yet deal with reused sockets.

* Do not use `io.Closer`.

* Use an inline error check.

* Use shared listeners and packet connections.

This allows us to reload a config while the existing one is still
running. They share the same underlying listener, which is actually
closed when the last user closes it.

* Close existing listeners once the new ones are serving.

* Elevate failure to stop listeners to `ERROR` level.

* Be more lenient in config validation to allow empty listeners or keys.

* Ensure the address is an IP address.

* Use `yaml.v3`.

* Move file reading back to `main.go`.

* Do not embed the `net.Listener` type.

* Use a `Service` object to abstract away some of the complex logic of managing listeners.

* Fix how we deal with legacy services.

* Remove commented out lines.

* Use `tcp` and `udp` types for direct listeners.

* Use a `ListenerManager` instead of globals to manage listener state.

* Add validation check that no two services have the same listener.

* Use channels to notify shared listeners they need to stop acceoting.

* Pass TCP timeout to service.

* Move go routine call up.

* Allow inserting single elements directly into the cipher list.

* Add the concept of a listener set to track existing listeners and close them all.

* Refactor how we create listeners.

We introduce shared listeners that allow us to keep an old config
running while we set up a new config. This is done by keeping track of
the usage of the listeners and only closing them when the last user is
done with the shared listener.

* Update comments.

* `go mod tidy`.

* refactor: don't link the TCP handler to a specific listener

* Protect new cipher handling methods with mutex.

* Move `listeners.go` under `/service`.

* Use callback instead of passing in key and manager.

* Move config start into a go routine for easier cleanup.

* Make a `StreamListener` type.

* Rename `closeFunc` to `onCloseFunc`.

* Rename `globalListener`.

* Don't track usage in the shared listeners.

* Add `getAddr()` to avoid some duplicate code.

* Move listener set creation out of the inner function.

* Remove `PushBack()` from `CipherList`.

* Move listener set to `main.go`.

* Close the accept channel with an atomic value.

* Update comment.

* Address review comments.

* Close before deleting key.

* `server.Stop()` does not return a value

* Add a comment for `StreamListener`.

* Do not delete the listener from the manager until the last user has closed it.

* Consolidate usage counting inside a `listenAddress` type.

* Remove `atomic.Value`.

* Add some missing comments.

* address review comments

* Add type guard for `sharedListener`.

* Stop the existing config in a goroutine.

* Add a TODO to wait for all handlers to be stopped.

* Run `stopConfig` in a goroutine in `Stop()` as well.

* Create a `TCPListener` that implements a `StreamListener`.

* Track close functions instead of the entire listener, which is not needed.

* Delegate usage tracking to a reference counter.

* Remove the `Get()` method from `refCount`.

* Return immediately.

* Rename `shared` to `virtual` as they are not actually shared.

* Simplify `listenAddr`.

* Fix use of the ref count.

* Add simple test case for early closing of stream listener.

* Add tests for creating stream listeners.

* Create handlers on demand.

* Refactor create methods.

* Address review comments.

* Use a mutex to ensure another user doesn't acquire a new closer while we're closing it.

* Move mutex up.

* Manage the ref counting next to the listener creation.

* Do the lazy initialization inside an anonymous function.

* Fix concurrent access to `acceptCh` and `closeCh`.

* Use `/` in key instead of `-`.

* Return error from stopping listeners.

* Use channels to ensure `virtualPacketConn`s get closed.

* Add more test cases for packet listeners.

* Only log errors from stopping old configs.

* Remove the `closed` field from the virtual listeners.

* Remove the `RefCount`.

* Implement channel-based packet read for virtual connections.

* Use a done channel.

* Set listeners and `onCloseFunc`'s to nil when closing.

* Set `onCloseFunc`'s to nil when closing.

* Fix race condition.

* Add some benchmarks for listener manager.

* Add structure logging with `slog`.

* Structure forgotten log.

* Another forgotten log.

* Remove IPInfo logic from TCP and UDP handling into the metrics collector.

* Refactor metrics into separate collectors.

* Rename some types to remove `Collector` suffix.

* Use an LRU cache to manage the ipInfos for Prometheus metrics.

* Use `nil` instead of `context.TODO()`.

* Use `LogAttrs` for `debug...()` log functions.

* Update logging in `metrics.go`.

* Fix another race condition.

* Revert renaming.

* Replace LRU cache with a simpler map that expires unused items.

* Move `SetBuildInfo()` call up.

* refactor: change `outlineMetrics` to implement the `prometheus.Collector` interface

* Address review comments.

* Refactor collectors so the connections/associations keep track of the connection metrics.

* Address review comments.

* Make metrics interfaces for bytes consistently use `int64`.

* Add license header.

* Support multi-module workspaces so we can develop Caddy and ss-server at the same time.

* Rename `Collector` to `Metrics`.

* Move service creation into the service package so it can be re-used by Caddy.

* Ignore custom Caddy binary.

* refactor: create re-usable service that can be re-used by Caddy

* Remove need to return errors in opt functions.

* Move the service into `shadowsocks.go`.

* Add Caddy module with app and handler.

* Refactor metrics to not share with Caddy.

* Set Prometheus metrics handler.

* Catch already registered collectors instead of using `once.Sync`.

* refactor: pass in logger to service so caller can control logs

* Fix test.

* Add `--watch` flag to README.

* Remove changes moved to another PR.

* Remove arguments from `Logger()`.

* Use `slog` instead of `zap`.

* Log error in `Provision()` instead of `defineMetrics()`.

* Do not panic on bad metrics registrations.

* Check if the cast to `OutlineApp` is ok.

* Remove `version` from the config.

* Use `outline_` prefix for Caddy metrics.

* Remove unused `NatTimeoutSec` config option.

* Move initialization of handlers to the constructor.

* Pass a `list.List` instead of a `CipherList`.

* Rename `SSServer` to `OutlineServer`.

* refactor: make connection metrics optional

* Make setting the logger a setter function.

* Revert "Pass a `list.List` instead of a `CipherList`."

This reverts commit 1259af8.

* Create noop metrics if nil.

* Revert some more changes.

* Use a noop metrics struct if no metrics provided.

* Add noop implementation for `ShadowsocksConnMetrics`.

* Move logger arg.

* Resolve nil metrics.

* Set logger explicitly to `noopLogger` in service creation.

* Address review comments.

* Set `noopLogger` in `NewShadowsocksStreamAuthenticator()` if nil.

* Fix logger reference.

* Add TODO comment to persist replay cache.

* Remove use of zap.

* Use a `ModuleRegistration` approach as per review comments.

* Add regression test that covers the issue.

* Fix the test.

* Use consistent comments.

* Use a `noopLogger` if `SetLogger()` is called with `nil`.

* Update tests.

* Remove empty newline.

* Update `outline-ss-server` and remove now unused `zap`.

* Update `outline-ss-server`.

* Use concrete `slog.Logger` instead of `Logger` interface now that we don't need a zap adapter for Caddy.

* Move `WithLogger()` down.

* Remove `nil` check.

* Use `math.MaxInt` to make sure no error log records are created.
  • Loading branch information
sbruens committed Sep 23, 2024
1 parent 5d3d6db commit cb5965f
Show file tree
Hide file tree
Showing 8 changed files with 1,155 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@
# Go workspace
go.work
go.work.sum

# Custom caddy binary
/caddy/caddy
25 changes: 25 additions & 0 deletions caddy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Caddy Module

The Caddy module provides an app and handler for Caddy Server
(https://caddyserver.com/) allowing it to turn any Caddy Server into an Outline
Shadowsocks backend.

## Prerequisites

- [xcaddy](https://github.com/caddyserver/xcaddy)

## Usage

From this directory, build and run a custom binary with `xcaddy`:

```sh
xcaddy run --config config_example.json --watch
```

In a separate window, confirm you can fetch a page using this server:

```sh
go run github.com/Jigsaw-Code/outline-sdk/x/examples/fetch -transport "ss://chacha20-ietf-poly1305:Secret1@:9000" http://ipinfo.io
```

Prometheus metrics are available on http://localhost:9091/metrics.
131 changes: 131 additions & 0 deletions caddy/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2024 The Outline Authors
//
// 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 caddy provides an app and handler for Caddy Server (https://caddyserver.com/)
// allowing it to turn any handler into one supporting the Vulcain protocol.

package caddy

import (
"errors"
"log/slog"

outline_prometheus "github.com/Jigsaw-Code/outline-ss-server/prometheus"
outline "github.com/Jigsaw-Code/outline-ss-server/service"
"github.com/caddyserver/caddy/v2"
"github.com/prometheus/client_golang/prometheus"
)

const outlineModuleName = "outline"

func init() {
caddy.RegisterModule(ModuleRegistration{
ID: outlineModuleName,
New: func() caddy.Module { return new(OutlineApp) },
})
}

type ShadowsocksConfig struct {
ReplayHistory int `json:"replay_history,omitempty"`
}

type OutlineApp struct {
ShadowsocksConfig *ShadowsocksConfig `json:"shadowsocks,omitempty"`

ReplayCache outline.ReplayCache
logger *slog.Logger
Metrics outline.ServiceMetrics
buildInfo *prometheus.GaugeVec
}

var (
_ caddy.App = (*OutlineApp)(nil)
_ caddy.Provisioner = (*OutlineApp)(nil)
)

func (OutlineApp) CaddyModule() caddy.ModuleInfo {
return caddy.ModuleInfo{ID: outlineModuleName}
}

// Provision sets up Outline.
func (app *OutlineApp) Provision(ctx caddy.Context) error {
app.logger = ctx.Slogger()

app.logger.Info("provisioning app instance")

if app.ShadowsocksConfig != nil {
// TODO: Persist replay cache across config reloads.
app.ReplayCache = outline.NewReplayCache(app.ShadowsocksConfig.ReplayHistory)
}

if err := app.defineMetrics(); err != nil {
app.logger.Error("failed to define Prometheus metrics", "err", err)
}
// TODO: Set version at build time.
app.buildInfo.WithLabelValues("dev").Set(1)
// TODO: Add replacement metrics for `shadowsocks_keys` and `shadowsocks_ports`.

return nil
}

func (app *OutlineApp) defineMetrics() error {
r := prometheus.WrapRegistererWithPrefix("outline_", prometheus.DefaultRegisterer)

var err error
buildInfo := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "build_info",
Help: "Information on the outline-ss-server build",
}, []string{"version"})
app.buildInfo, err = registerCollector(r, buildInfo)
if err != nil {
return err
}

// TODO: Allow the configuration of ip2info.
metrics, err := outline_prometheus.NewServiceMetrics(nil)
if err != nil {
return err
}
app.Metrics, err = registerCollector(r, metrics)
if err != nil {
return err
}
return nil
}

func registerCollector[T prometheus.Collector](registerer prometheus.Registerer, coll T) (T, error) {
if err := registerer.Register(coll); err != nil {
are := &prometheus.AlreadyRegisteredError{}
if !errors.As(err, are) {
// This collector has been registered before. This is expected during a config reload.
coll = are.ExistingCollector.(T)
} else {
// Something else went wrong.
return coll, err
}
}
return coll, nil
}

// Start starts the App.
func (app *OutlineApp) Start() error {
app.logger.Debug("started app instance")
return nil
}

// Stop stops the App.
func (app *OutlineApp) Stop() error {
app.logger.Debug("stopped app instance")
return nil
}
96 changes: 96 additions & 0 deletions caddy/config_example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
{
"admin": {
"disabled": true
},
"logging": {
"logs": {
"default": {"level":"DEBUG", "encoder": {"format":"console"}}
}
},
"apps": {
"http": {
"servers": {
"": {
"listen": [
":9091"
],
"routes": [
{
"match": [
{
"path": [
"/metrics"
]
}
],
"handle": [
{
"disable_openmetrics": true,
"handler": "metrics"
}
]
}
]
}
}
},
"layer4": {
"servers": {
"1": {
"listen": [
"tcp/[::]:9000",
"udp/[::]:9000"
],
"routes": [
{
"handle": [
{
"handler": "shadowsocks",
"keys": [
{
"id": "user-0",
"cipher": "chacha20-ietf-poly1305",
"secret": "Secret0"
},
{
"id": "user-1",
"cipher": "chacha20-ietf-poly1305",
"secret": "Secret1"
}
]
}
]
}
]
},
"2": {
"listen": [
"tcp/[::]:9001",
"udp/[::]:9001"
],
"routes": [
{
"handle": [
{
"handler": "shadowsocks",
"keys": [
{
"id": "user-2",
"cipher": "chacha20-ietf-poly1305",
"secret": "Secret2"
}
]
}
]
}
]
}
}
},
"outline": {
"shadowsocks": {
"replay_history": 10000
}
}
}
}
125 changes: 125 additions & 0 deletions caddy/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
module github.com/Jigsaw-Code/outline-ss-server/caddy

go 1.23

require (
github.com/Jigsaw-Code/outline-sdk v0.0.16
github.com/Jigsaw-Code/outline-ss-server v1.7.2
github.com/caddyserver/caddy/v2 v2.8.4
github.com/mholt/caddy-l4 v0.0.0-20240812213304-afa78d72257b
github.com/prometheus/client_golang v1.20.0
)

require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/caddyserver/certmagic v0.21.3 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chzyer/readline v1.5.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/dgraph-io/badger v1.6.2 // indirect
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/golang/glog v1.2.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/cel-go v0.20.1 // indirect
github.com/google/pprof v0.0.0-20240207164012-fb44976bdcd5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.3 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/libdns/libdns v0.2.2 // indirect
github.com/lmittmann/tint v1.0.5 // indirect
github.com/manifoldco/promptui v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/acmez/v2 v2.0.1 // indirect
github.com/miekg/dns v1.1.59 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.15.0 // indirect
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
github.com/oschwald/geoip2-golang v1.8.0 // indirect
github.com/oschwald/maxminddb-golang v1.10.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/quic-go v0.44.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/slackhq/nebula v1.7.2 // indirect
github.com/smallstep/certificates v0.26.1 // indirect
github.com/smallstep/nosql v0.6.1 // indirect
github.com/smallstep/pkcs7 v0.0.0-20231024181729-3b98ecc1ca81 // indirect
github.com/smallstep/scep v0.0.0-20231024192529-aee96d7ad34d // indirect
github.com/smallstep/truststore v0.13.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/spf13/cobra v1.8.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/tailscale/tscert v0.0.0-20240517230440-bbccfbf48933 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.etcd.io/bbolt v1.3.9 // indirect
go.step.sm/cli-utils v0.9.0 // indirect
go.step.sm/crypto v0.45.0 // indirect
go.step.sm/linkedca v0.20.1 // indirect
go.uber.org/automaxprocs v1.5.3 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.2.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/crypto/x509roots/fallback v0.0.0-20240507223354-67b13616a595 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240506185236-b8a5c65736ae // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect
google.golang.org/grpc v1.63.2 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
)
Loading

0 comments on commit cb5965f

Please sign in to comment.