Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rulesets #39

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21.0
require (
github.com/VictoriaMetrics/metrics v1.23.1
github.com/andybalholm/cascadia v1.3.1
github.com/antonmedv/expr v1.15.2
github.com/cardigann/harhar v0.0.0-20161005032312-acb91b7a8682
github.com/hashicorp/go-envparse v0.1.0
github.com/jmoiron/sqlx v1.3.5
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ github.com/VictoriaMetrics/metrics v1.23.1 h1:/j8DzeJBxSpL2qSIdqnRFLvQQhbJyJbbEi
github.com/VictoriaMetrics/metrics v1.23.1/go.mod h1:rAr/llLpEnAdTehiNlUxKgnjcOuROSzpw0GvjpEbvFc=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/antonmedv/expr v1.15.2 h1:afFXpDWIC2n3bF+kTZE1JvFo+c34uaM3sTqh8z0xfdU=
github.com/antonmedv/expr v1.15.2/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE=
github.com/cardigann/harhar v0.0.0-20161005032312-acb91b7a8682 h1:Ce5LRUcDnICPpYjWych45AXKaV61l9oqqfMd1hORNPg=
github.com/cardigann/harhar v0.0.0-20161005032312-acb91b7a8682/go.mod h1:cDq9S+BVx7XyKnnivCLcKW1oUTnXHkUSs6+LFs4ZrXA=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
Expand Down Expand Up @@ -32,12 +36,16 @@ github.com/mmcloughlin/geohash v0.10.0/go.mod h1:oNZxQo5yWJh0eMQEP/8hwQuVx9Z9tjw
github.com/pg9182/ip2x v1.0.0 h1:aNIWIjzFYmaVHIsbT6OYDVgiySQkgjmecySvjXOCgsU=
github.com/pg9182/ip2x v1.0.0/go.mod h1:iJzts7yWZDWUaldqNGFVOZ0MW9uYrGk1hv9R/wTJdNQ=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w=
github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
Expand All @@ -57,3 +65,5 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
43 changes: 43 additions & 0 deletions pkg/api/api0/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package api0

import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
Expand All @@ -30,6 +31,7 @@ import (
"github.com/r2northstar/atlas/pkg/eax"
"github.com/r2northstar/atlas/pkg/metricsx"
"github.com/r2northstar/atlas/pkg/nspkt"
"github.com/r2northstar/atlas/pkg/nsrule"
"github.com/r2northstar/atlas/pkg/origin"
"github.com/rs/zerolog/hlog"
"golang.org/x/mod/semver"
Expand Down Expand Up @@ -110,6 +112,9 @@ type Handler struct {
// empty region and no error if no region is to be assigned.
GetRegion func(netip.Addr, ip2x.Record) (string, error)

// Rules provides a ruleset to apply.
Rules *nsrule.RuleSet

metricsInit sync.Once
metricsObj apiMetrics

Expand All @@ -127,6 +132,14 @@ type connectState struct {
gotPdata atomic.Bool
}

type rulesContextKey struct{}

type rulesContextValue struct {
e nsrule.Env
s *nsrule.RuleSet
t nsrule.Tags
}

// ServeHTTP routes requests to Handler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var notPanicked bool // this lets us catch panics without swallowing them
Expand All @@ -136,6 +149,15 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}()

if h.Rules != nil {
r = r.WithContext(context.WithValue(r.Context(), rulesContextKey{}, &rulesContextValue{
e: nsrule.NewEnv(),
s: h.Rules,
t: make(nsrule.Tags),
}))
h.EvalRules(r, nil)
}

w.Header().Set("Server", "Atlas")

switch r.URL.Path {
Expand Down Expand Up @@ -174,6 +196,27 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
notPanicked = true
}

func (h *Handler) EvalRules(r *http.Request, update func(e nsrule.Env)) {
if v := r.Context().Value(rulesContextKey{}); v != nil {
t := time.Now()
v := v.(*rulesContextValue)
if update != nil {
update(v.e)
}
for _, err := range v.s.Evaluate(v.e, v.t) {
hlog.FromRequest(r).Warn().Err(err).Msg("failed to evaluate rule")
}
h.m().rule_evaluation_time_seconds.UpdateDuration(t)
}
}

func (h *Handler) Tags(r *http.Request) nsrule.Tags {
if v := r.Context().Value(rulesContextKey{}); v != nil {
return v.(*rulesContextValue).t
}
return nsrule.Tags{}
}

// CheckLauncherVersion checks if the r was made by NorthstarLauncher and if it
// is at least MinimumLauncherVersion.
func (h *Handler) CheckLauncherVersion(r *http.Request, client bool) bool {
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/api0/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ type apiMetrics struct {
fail_other_error *metrics.Counter
http_method_not_allowed *metrics.Counter
}
rule_evaluation_time_seconds *metrics.Histogram
}

func (h *Handler) Metrics() *metrics.Set {
Expand Down Expand Up @@ -474,6 +475,7 @@ func (h *Handler) m() *apiMetrics {
mo.player_pdata_requests_total.fail_pdata_invalid = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="fail_pdata_invalid"}`)
mo.player_pdata_requests_total.fail_other_error = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="fail_other_error"}`)
mo.player_pdata_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_player_pdata_requests_total{result="http_method_not_allowed"}`)
mo.rule_evaluation_time_seconds = mo.set.NewHistogram(`atlas_api0_rule_evaluation_time_seconds`)
})

// ensure we initialized everything
Expand Down
3 changes: 3 additions & 0 deletions pkg/atlas/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ type Config struct {
// 192.168.0.0/24=1.2.3.4).
DevMapIP []string `env:"ATLAS_DEV_MAP_IP"`

// The path to a directory containing lexically-sorted rulesets.
Rules string `env:"ATLAS_RULES"`

// The maximum number of gameservers to allow. If -1, no limit is applied.
API0_MaxServers int `env:"ATLAS_API0_MAX_SERVERS=1000"`

Expand Down
23 changes: 23 additions & 0 deletions pkg/atlas/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/r2northstar/atlas/pkg/eax"
"github.com/r2northstar/atlas/pkg/memstore"
"github.com/r2northstar/atlas/pkg/nspkt"
"github.com/r2northstar/atlas/pkg/nsrule"
"github.com/r2northstar/atlas/pkg/origin"
"github.com/r2northstar/atlas/pkg/regionmap"
"github.com/rs/zerolog"
Expand All @@ -50,6 +51,7 @@ type Server struct {
API0 *api0.Handler
Middleware []func(http.Handler) http.Handler
TLSConfig *tls.Config
Rules *nsrule.RuleSet

reload []func()
closed bool
Expand Down Expand Up @@ -78,6 +80,26 @@ func NewServer(c *Config) (*Server, error) {

s.NotifySocket = c.NotifySocket

if c.Rules != "" {
s.Rules = new(nsrule.RuleSet)
if p, err := filepath.Abs(c.Rules); err == nil {
var err1 error
reload := func() {
if err := s.Rules.LoadFS(os.DirFS(p)); err != nil {
s.Logger.Err(err).Msgf("failed to load rules from %q", p)
err1 = fmt.Errorf("parse rulesets from %q: %w", p, err)
return
}
}
if reload(); err1 != nil {
return nil, fmt.Errorf("initialize rules: %w", err1)
}
s.reload = append(s.reload, reload)
} else {
return nil, fmt.Errorf("initialize rules: resolve path: %w", err)
}
}

if c.Web != "" {
if p, err := filepath.Abs(c.Web); err == nil {
var redirects sync.Map
Expand Down Expand Up @@ -285,6 +307,7 @@ func NewServer(c *Config) (*Server, error) {
MinimumLauncherVersionServer: c.API0_MinimumLauncherVersionServer,
TokenExpiryTime: c.API0_TokenExpiryTime,
AllowGameServerIPv6: c.API0_AllowGameServerIPv6,
Rules: s.Rules,
}
if v := c.API0_MinimumLauncherVersion; v != "" {
if s.API0.MinimumLauncherVersionClient == "" {
Expand Down
89 changes: 89 additions & 0 deletions pkg/nsrule/env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package nsrule

import (
"maps"
"reflect"

"github.com/antonmedv/expr"
)

// Env contains data used to evaluate rules.
type Env map[string]any

var (
dummyEnv = Env{}
defaultEnv = Env{}
extraOptions []expr.Option
)

// NewEnv shallow-copies the default values into a new Env.
func NewEnv() Env {
return maps.Clone(defaultEnv)
}

func define[T any](name string, def T, optional bool) func(Env, T) {
if name == "" {
panic("define: name is required")
}
if _, ok := dummyEnv[name]; ok {
panic("define: name is already used")
}
dummyEnv[name] = def
if !optional {
defaultEnv[name] = def
}
return func(e Env, v T) { e[name] = v }
}

// Define registers a variable with the provided name defaulting to the zero
// value. For parse-time expression type-checking to work, T should not be any.
func Define[T any](name string) func(Env, T) {
var zero T
return define[T](name, zero, false)
}

// DefineOptional registers an optional variable with the provided name. For parse-time
// expression type-checking to work, T should not be any.
func DefineOptional[T any](name string) func(Env, T) {
var zero T
return define[T](name, zero, true)
}

// DefineDefault registers a variable with the provided name. For parse-time
// expression type-checking to work, T should not be any.
func DefineDefault[T any](name string, def T) func(Env, T) {
return define[T](name, def, false)
}

// DefineOperator overloads an operator.
func DefineOperator[T any](op string, fn T) string {
name := op + " " + reflect.TypeOf(fn).String()
extraOptions = append(extraOptions, expr.Operator(op, name))
define[T](name, fn, false)
return name
}

// DefineOperatorCompare is shorthand for using DefineOperator to override
// comparison operators.
func DefineOperatorCompare[A, B any](cmp func(a A, b B) int) []string {
var ops []string
ops = append(ops, DefineOperator("<", func(a A, b B) bool {
return cmp(a, b) < 0
}))
ops = append(ops, DefineOperator("<=", func(a A, b B) bool {
return cmp(a, b) <= 0
}))
ops = append(ops, DefineOperator("==", func(a A, b B) bool {
return cmp(a, b) == 0
}))
ops = append(ops, DefineOperator("!=", func(a A, b B) bool {
return cmp(a, b) != 0
}))
ops = append(ops, DefineOperator(">=", func(a A, b B) bool {
return cmp(a, b) >= 0
}))
ops = append(ops, DefineOperator(">", func(a A, b B) bool {
return cmp(a, b) > 0
}))
return ops
}
Loading