Sets from Jarl: sets holding the addresses of names in Jarl
A set can take its addresses from Jarl: every A and AAAA record of its
names (NAME@VIEW), CNAMEs followed within Jarl's zones in the same view.
housecarld reads them every jarl.refresh and after each change, keeps
only the addresses within the zones and networks the set allows, and
keeps what it last read (saved in the database and the boot ruleset)
while Jarl can't be reached. A change of the addresses applies the
ruleset again without a history entry and is audited (set.jarl).
hc set add --jarl --within, hc set jarl; the kind "from Jarl" and
Addresses… on the Sets page; GET /sets/{name}/jarl and /jarl/refresh;
validation codes set_jarl_*, status warnings jarl_*; docs/jarl.md.
e2e phase with scripts/fakejarl.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
50dce02746
commit
68ba2d06c5
37 files changed
+2315
-59
No files matched your search
+132
-1
@@ -271,6 +271,9 @@ func setCmd() *cobra.Command {
|
||||
fmt.Fprintln(w, "NAME\tENTRIES\tCOMMENT")
|
||||
for _, s := range ss {
|
||||
entries := list(s.Entries)
|
||||
if s.Jarl != nil {
|
||||
entries = "from Jarl: " + strings.Join(jarlNames(s.Jarl), ", ")
|
||||
}
|
||||
if s.Dynamic {
|
||||
entries = "dynamic"
|
||||
if s.Timeout > 0 {
|
||||
@@ -290,6 +293,11 @@ func setCmd() *cobra.Command {
|
||||
fmt.Fprintf(w, "Timeout:\t%s\n", fmtSeconds(s.Timeout))
|
||||
}
|
||||
}
|
||||
if s.Jarl != nil {
|
||||
fmt.Fprintf(w, "From Jarl:\t%s\n", strings.Join(jarlNames(s.Jarl), ", "))
|
||||
fmt.Fprintf(w, "Within:\t%s\n", strings.Join(s.Jarl.Within, ", "))
|
||||
fmt.Fprintf(w, "Addresses:\thc set jarl %s\n", s.Name)
|
||||
}
|
||||
for i, e := range s.Entries {
|
||||
label := ""
|
||||
if i == 0 {
|
||||
@@ -303,18 +311,29 @@ func setCmd() *cobra.Command {
|
||||
}))
|
||||
var dynamic bool
|
||||
var timeout, comment string
|
||||
var jarlN, within []string
|
||||
add := &cobra.Command{
|
||||
Use: "add NAME [ENTRY...]", Short: "Add a set", Args: cobra.MinimumNArgs(1),
|
||||
Long: `Adds a set of addresses, prefixes and ranges, IPv4 and IPv6 mixed:
|
||||
|
||||
hc set add trusted 192.168.1.0/24 192.168.2.0/24 2001:db8:1::/64
|
||||
hc set add guests --dynamic --timeout 24h
|
||||
hc set add nas --jarl nas.example.com@internal --within lan
|
||||
|
||||
A dynamic set takes entries at run time, each with its own timeout ('hc
|
||||
set entries', or the API with a token for that set alone: a captive
|
||||
portal's signed-in clients).`,
|
||||
portal's signed-in clients).
|
||||
|
||||
A Jarl set holds the addresses of names in Jarl (their A and AAAA
|
||||
records, CNAMEs followed), read again every minute. --jarl takes
|
||||
NAME@VIEW, or NAME when only one view has it; --within says where its
|
||||
addresses may be (zones: their connected networks; or networks), and the
|
||||
others are left out ('hc set jarl NAME' shows them). docs/jarl.md.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
s := model.AddressSet{Name: args[0], Entries: args[1:], Dynamic: dynamic, Comment: comment}
|
||||
if len(jarlN) > 0 || len(within) > 0 {
|
||||
s.Jarl = jarlSource(jarlN, within)
|
||||
}
|
||||
if timeout != "" {
|
||||
n, err := parseSeconds(timeout)
|
||||
if err != nil {
|
||||
@@ -327,6 +346,8 @@ portal's signed-in clients).`,
|
||||
}
|
||||
add.Flags().BoolVar(&dynamic, "dynamic", false, "entries are added at run time")
|
||||
add.Flags().StringVar(&timeout, "timeout", "", "a dynamic set's default entry lifetime (1h, 7d)")
|
||||
add.Flags().StringArrayVar(&jarlN, "jarl", nil, "a name in Jarl whose addresses the set holds: NAME or NAME@VIEW (repeat)")
|
||||
add.Flags().StringArrayVar(&within, "within", nil, "a Jarl set: a zone or network its addresses must lie in (repeat)")
|
||||
add.Flags().StringVar(&comment, "comment", "", "a comment")
|
||||
cmd.AddCommand(add)
|
||||
var addE, removeE, entries []string
|
||||
@@ -350,6 +371,23 @@ portal's signed-in clients).`,
|
||||
}
|
||||
p["entries"] = editList(cur, addE, removeE)
|
||||
}
|
||||
if fl.Changed("jarl") || fl.Changed("within") {
|
||||
s, err := get[model.AddressSet](objPath("sets", args[0]), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
j := s.Jarl
|
||||
if j == nil {
|
||||
j = &model.JarlSource{}
|
||||
}
|
||||
if fl.Changed("jarl") {
|
||||
j.Names = jarlSource(jarlN, nil).Names
|
||||
}
|
||||
if fl.Changed("within") {
|
||||
j.Within = within
|
||||
}
|
||||
p["jarl"] = j
|
||||
}
|
||||
if fl.Changed("timeout") {
|
||||
n, err := parseSeconds(timeout)
|
||||
if err != nil {
|
||||
@@ -371,11 +409,104 @@ portal's signed-in clients).`,
|
||||
f.StringArrayVar(&addE, "add", nil, "add an entry")
|
||||
f.StringArrayVar(&removeE, "remove", nil, "remove an entry")
|
||||
f.StringVar(&timeout, "timeout", "", "a dynamic set's default entry lifetime")
|
||||
f.StringArrayVar(&jarlN, "jarl", nil, "replace a Jarl set's names: NAME or NAME@VIEW (repeat)")
|
||||
f.StringArrayVar(&within, "within", nil, "replace where a Jarl set's addresses may be: zones, networks (repeat)")
|
||||
f.StringVar(&comment, "comment", "", "a comment")
|
||||
f.StringVar(&name, "name", "", "a new name")
|
||||
cmd.AddCommand(update)
|
||||
cmd.AddCommand(deleteCmd("sets", "set"))
|
||||
cmd.AddCommand(entriesCmd())
|
||||
cmd.AddCommand(jarlCmd())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func jarlSource(names, within []string) *model.JarlSource {
|
||||
j := &model.JarlSource{Names: []model.JarlName{}, Within: within}
|
||||
for _, n := range names {
|
||||
j.Names = append(j.Names, model.ParseJarlName(n))
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
func jarlNames(j *model.JarlSource) []string {
|
||||
var out []string
|
||||
for _, n := range j.Names {
|
||||
out = append(out, n.String())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// jarlCmd shows what a Jarl set holds, name by name.
|
||||
func jarlCmd() *cobra.Command {
|
||||
var refresh bool
|
||||
cmd := &cobra.Command{Use: "jarl SET", Short: "What a Jarl set holds: each name's addresses in Jarl, and which are used", Args: cobra.ExactArgs(1),
|
||||
Long: `Shows a Jarl set's names with the addresses Jarl gives them (CNAMEs
|
||||
followed), whether each is used or why it is left out (not within the
|
||||
set's zones and networks), and when Jarl last answered. --refresh reads
|
||||
Jarl first.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
c, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var js api.JarlSet
|
||||
if refresh {
|
||||
err = c.Do(ctx(), "POST", objPath("sets", args[0])+"/jarl/refresh", nil, nil, &js)
|
||||
} else {
|
||||
err = c.Do(ctx(), "GET", objPath("sets", args[0])+"/jarl", nil, nil, &js)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return output(js, func(w io.Writer) {
|
||||
if !js.Configured {
|
||||
fmt.Fprintln(w, "Jarl isn't configured (jarl.server in housecarl.yaml): the set keeps the addresses it last had.")
|
||||
}
|
||||
if js.Error != "" {
|
||||
fmt.Fprintf(w, "Reading Jarl failed: %s (the set keeps the addresses it had)\n", js.Error)
|
||||
}
|
||||
fmt.Fprintf(w, "Set:\t%s\n", js.Set)
|
||||
fmt.Fprintf(w, "Within:\t%s\n", strings.Join(js.Within, ", "))
|
||||
fmt.Fprintf(w, "Holds:\t%s\n", list(js.Entries))
|
||||
if js.Read != nil {
|
||||
fmt.Fprintf(w, "Jarl read:\t%s\n", js.Read.Local().Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, "NAME\tVIEW\tADDRESS\tUSED")
|
||||
for _, n := range js.Names {
|
||||
label := n.Name
|
||||
if len(n.Chain) > 0 {
|
||||
label += " → " + strings.Join(n.Chain, " → ")
|
||||
}
|
||||
view := n.View
|
||||
if view == "" {
|
||||
view = n.Found
|
||||
}
|
||||
if view == "" {
|
||||
view = "-"
|
||||
}
|
||||
switch {
|
||||
case n.Problem != "":
|
||||
fmt.Fprintf(w, "%s\t%s\t-\tno: %s\n", label, view, n.Problem)
|
||||
case len(n.Addresses) == 0 && n.Error != "":
|
||||
fmt.Fprintf(w, "%s\t%s\t-\tnot read: %s\n", label, view, n.Error)
|
||||
case len(n.Addresses) == 0:
|
||||
fmt.Fprintf(w, "%s\t%s\t-\tnot read yet\n", label, view)
|
||||
}
|
||||
for _, a := range n.Addresses {
|
||||
used := "yes"
|
||||
if !a.Used {
|
||||
used = "no: " + a.Why
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", label, view, a.Address, used)
|
||||
}
|
||||
if n.Error != "" && len(n.Addresses) > 0 {
|
||||
fmt.Fprintf(w, "%s\t%s\t\t(the last read failed: %s; these are from before)\n", label, view, n.Error)
|
||||
}
|
||||
}
|
||||
})
|
||||
}}
|
||||
cmd.Flags().BoolVar(&refresh, "refresh", false, "read Jarl now")
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
+33
-5
@@ -30,6 +30,7 @@ import (
|
||||
"housecarl/internal/engine"
|
||||
"housecarl/internal/hostif"
|
||||
"housecarl/internal/jarl"
|
||||
"housecarl/internal/jarlsets"
|
||||
"housecarl/internal/linkwatch"
|
||||
"housecarl/internal/model"
|
||||
"housecarl/internal/names"
|
||||
@@ -165,19 +166,43 @@ func serveCmd() *cobra.Command {
|
||||
for _, p := range eng.Problems() {
|
||||
a.log.Warn("configuration problem: "+p.Message, "object", p.Object, "code", p.Code)
|
||||
}
|
||||
var acme *certs.Manager
|
||||
if a.cfg.TLS.ACME.Domain != "" {
|
||||
var jc *jarl.Client
|
||||
if a.cfg.Jarl.Enabled() {
|
||||
tok, err := a.cfg.Jarl.ReadToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
jc, err := jarl.New(a.cfg.Jarl.Server, tok, a.cfg.Jarl.CACert, a.cfg.Jarl.Insecure)
|
||||
if err != nil {
|
||||
if jc, err = jarl.New(a.cfg.Jarl.Server, tok, a.cfg.Jarl.CACert, a.cfg.Jarl.Insecure); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var acme *certs.Manager
|
||||
if a.cfg.TLS.ACME.Domain != "" {
|
||||
if jc == nil {
|
||||
return errors.New("tls.acme needs Jarl: set jarl.server and its token (docs/certificates.md)")
|
||||
}
|
||||
acme = certs.New(ctx, certs.Config{ACME: a.cfg.TLS.ACME, Version: version.Short()}, jc, a.store, a.log)
|
||||
go acme.Run(ctx)
|
||||
}
|
||||
// The sets whose addresses come from Jarl: what was last read
|
||||
// is put in force at once, Jarl is read again soon after.
|
||||
jarlSets := &jarlsets.Resolver{Interval: time.Duration(a.cfg.Jarl.Refresh), State: eng.State, Log: a.log,
|
||||
Apply: func(ctx context.Context) error {
|
||||
if err := eng.ApplyCurrent(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
return fw.SaveBoot(eng.State())
|
||||
},
|
||||
Audit: func(ctx context.Context, set, detail string) {
|
||||
eng.Audit(ctx, &auth.Principal{Method: "jarl"}, "set.jarl", set, "success", detail)
|
||||
},
|
||||
Load: func(ctx context.Context) (string, error) { return a.store.GetMeta(ctx, jarlsets.MetaKey) },
|
||||
Save: func(ctx context.Context, v string) error { return a.store.SetMeta(ctx, jarlsets.MetaKey, v) }}
|
||||
if jc != nil {
|
||||
jarlSets.Source = jc
|
||||
}
|
||||
jarlSets.Restore(ctx)
|
||||
fw.JarlSets = jarlSets
|
||||
var resolver *names.Resolver
|
||||
if a.cfg.Names.Enabled {
|
||||
resolver = names.New(a.cfg.Names.Resolver, time.Duration(a.cfg.Names.Cache))
|
||||
@@ -186,8 +211,9 @@ func serveCmd() *cobra.Command {
|
||||
Listen: a.cfg.Listen, TLSCertFile: a.cfg.TLS.CertFile, TLSKeyFile: a.cfg.TLS.KeyFile, TrustedProxies: a.cfg.Proxies(),
|
||||
SessionLifetime: time.Duration(a.cfg.Session.Lifetime), SessionIdle: time.Duration(a.cfg.Session.IdleTimeout),
|
||||
CookieSecure: a.cfg.Session.CookieSecure, Socket: a.cfg.Socket, ACME: acme,
|
||||
Shadow: a.cfg.Shadow.Enabled, ShadowPosition: a.cfg.Shadow.Position, Names: resolver,
|
||||
Shadow: a.cfg.Shadow.Enabled, ShadowPosition: a.cfg.Shadow.Position, Names: resolver, JarlSets: jarlSets,
|
||||
}, eng, fw, host, a.log, web.FS())
|
||||
jarlSets.Connected = srv.Connected
|
||||
// Put the ruleset in force: what was on probation when
|
||||
// housecarld stopped is rolled back first. An empty
|
||||
// configuration applies nothing: its default policies would
|
||||
@@ -217,6 +243,7 @@ func serveCmd() *cobra.Command {
|
||||
go func() {
|
||||
err := linkwatch.Watch(ctx, func() {
|
||||
host.Forget()
|
||||
jarlSets.Kick() // a zone's networks may have changed
|
||||
if !fw.FlowtableStale(eng.State()) {
|
||||
return
|
||||
}
|
||||
@@ -253,6 +280,7 @@ func serveCmd() *cobra.Command {
|
||||
}
|
||||
}()
|
||||
}
|
||||
go jarlSets.Run(ctx)
|
||||
go nflog.Listen(ctx, render.LogGroup, a.log, srv.LogPacket)
|
||||
go srv.RunQuotas(ctx)
|
||||
srv.SetConntrack(conntrack.Client{})
|
||||
|
||||
Reference in new issue
Block a user