mirror of https://github.com/0xERR0R/blocky.git
114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
// Package service exposes types to abstract services from the networking.
|
|
//
|
|
// The idea is that we build a set of services and a set of network endpoints (Listener).
|
|
// The services are then assigned to endpoints based on the address(es) they were configured for.
|
|
//
|
|
// Actual service to endpoint binding is not handled by the abstractions in this package as it is
|
|
// protocol specific.
|
|
// The general pattern is to make a "server" that wraps a service, and can then be started on an
|
|
// endpoint using a `Serve` method, similar to `http.Server`.
|
|
//
|
|
// To support exposing multiple compatible services on a single endpoint (example: DoH + metrics on a single port),
|
|
// services can implement `Merger`.
|
|
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/0xERR0R/blocky/util"
|
|
)
|
|
|
|
// Service is a network exposed service.
|
|
//
|
|
// It contains only the logic and user configured addresses it should be exposed on.
|
|
// Is is meant to be associated to one or more sockets via those addresses.
|
|
// Actual association with a socket is protocol specific.
|
|
type Service interface {
|
|
fmt.Stringer
|
|
|
|
// ServiceName returns the user friendly name of the service.
|
|
ServiceName() string
|
|
|
|
// ExposeOn returns the set of endpoints the service should be exposed on.
|
|
//
|
|
// They can be used to find listener(s) with matching configuration.
|
|
ExposeOn() []Endpoint
|
|
}
|
|
|
|
func svcString(s Service) string {
|
|
endpoints := util.ForEach(s.ExposeOn(), func(e Endpoint) string { return e.String() })
|
|
|
|
return fmt.Sprintf("%s on %s", s.ServiceName(), strings.Join(endpoints, ", "))
|
|
}
|
|
|
|
// Info can be embedded in structs to help implement Service.
|
|
type Info struct {
|
|
name string
|
|
endpoints []Endpoint
|
|
}
|
|
|
|
func NewInfo(name string, endpoints []Endpoint) Info {
|
|
return Info{
|
|
name: name,
|
|
endpoints: endpoints,
|
|
}
|
|
}
|
|
|
|
func (i *Info) ServiceName() string { return i.name }
|
|
func (i *Info) ExposeOn() []Endpoint { return i.endpoints }
|
|
func (i *Info) String() string { return svcString(i) }
|
|
|
|
// GroupByListener returns a map of listener and services grouped by configured address.
|
|
//
|
|
// Each input listener is a key in the map. The corresponding value is a service
|
|
// merged from all services with a matching address.
|
|
func GroupByListener(services []Service, listeners []Listener) (map[Listener]Service, error) {
|
|
res := make(map[Listener]Service, len(listeners))
|
|
unused := slices.Clone(services)
|
|
|
|
for _, listener := range listeners {
|
|
services := findAllCompatible(services, listener.Exposes())
|
|
if len(services) == 0 {
|
|
return nil, fmt.Errorf("found no compatible services for listener %s", listener)
|
|
}
|
|
|
|
svc, err := MergeAll(services)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot merge services configured for listener %s: %w", listener, err)
|
|
}
|
|
|
|
res[listener] = svc
|
|
|
|
for _, svc := range services {
|
|
if i := slices.Index(unused, svc); i != -1 {
|
|
unused = slices.Delete(unused, i, i+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(unused) != 0 {
|
|
return nil, fmt.Errorf("found no compatible listener for services: %v", unused)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// findAllCompatible returns the subset of services that use the given Listener.
|
|
func findAllCompatible(services []Service, endpoint Endpoint) []Service {
|
|
res := make([]Service, 0, len(services))
|
|
|
|
for _, svc := range services {
|
|
if isExposedOn(svc, endpoint) {
|
|
res = append(res, svc)
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func isExposedOn(svc Service, endpoint Endpoint) bool {
|
|
return slices.Index(svc.ExposeOn(), endpoint) != -1
|
|
}
|