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
@@ -247,15 +247,79 @@ func (c *checker) sets() {
|
||||
if len(s.Entries) > 0 && s.Dynamic {
|
||||
c.add(Error, "set_dynamic_entries", obj, "a dynamic set gets its entries at run time (hc set entries add, the Entries button), each with its timeout; leave out the configured ones, or make it a set that isn't dynamic")
|
||||
}
|
||||
if len(s.Entries) == 0 && !s.Dynamic {
|
||||
if len(s.Entries) == 0 && !s.Runtime() {
|
||||
c.add(Warning, "set_empty", obj, "the set is empty: rules using it match nothing")
|
||||
}
|
||||
if s.Jarl != nil {
|
||||
c.jarlSet(obj, s)
|
||||
}
|
||||
if s.Timeout < 0 || s.Timeout > 0 && !s.Dynamic {
|
||||
c.add(Error, "set_timeout", obj, "only a dynamic set has a timeout, of 1 second or more")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dnsLabel is one label of a name in Jarl (underscores allowed, as in
|
||||
// _service names).
|
||||
var dnsLabel = regexp.MustCompile(`^[a-z0-9_]([a-z0-9_-]{0,61}[a-z0-9_])?$`)
|
||||
|
||||
// jarlSet checks a set whose entries come from Jarl.
|
||||
func (c *checker) jarlSet(obj string, s *model.AddressSet) {
|
||||
j := s.Jarl
|
||||
if len(s.Entries) > 0 {
|
||||
c.add(Error, "set_jarl_entries", obj, "a Jarl set's entries are the addresses of its names in Jarl; leave out the configured ones, or make it a set that doesn't come from Jarl")
|
||||
}
|
||||
if s.Dynamic {
|
||||
c.add(Error, "set_jarl_dynamic", obj, "a set is dynamic or comes from Jarl, not both")
|
||||
}
|
||||
if len(j.Names) == 0 {
|
||||
c.add(Error, "set_jarl_names", obj, "name at least one name in Jarl, such as nas.example.com (or nas.example.com@internal for one view)")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, n := range j.Names {
|
||||
if !validDNSName(n.Name) {
|
||||
c.add(Error, "set_jarl_names", obj, "%q isn't a DNS name; give the full name, such as nas.example.com (wildcards aren't followed)", n.Name)
|
||||
continue
|
||||
}
|
||||
if strings.ContainsAny(n.View, " \t@") {
|
||||
c.add(Error, "set_jarl_names", obj, "%q isn't a view name: write NAME@VIEW, such as nas.example.com@internal", n.View)
|
||||
continue
|
||||
}
|
||||
if seen[n.String()] {
|
||||
c.add(Error, "set_jarl_names", obj, "%s is listed twice", n)
|
||||
}
|
||||
seen[n.String()] = true
|
||||
}
|
||||
if len(j.Within) == 0 {
|
||||
c.add(Error, "set_jarl_within", obj, "say where its addresses may be (within): zones, whose connected networks count, or networks such as 10.10.0.0/24; an address from Jarl elsewhere is left out, so a changed record can't open the firewall to anything")
|
||||
}
|
||||
for _, w := range j.Within {
|
||||
if a, err := model.ParseAddress(w); err == nil {
|
||||
if a.Prefix.IsValid() && a.Prefix.Bits() == 0 && !a.First.IsValid() {
|
||||
c.add(Warning, "set_jarl_anywhere", obj, "within %s lets any address in: whoever can change the names in Jarl decides what the set holds; name zones or networks instead", w)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if c.st.Zone(w) == nil {
|
||||
c.add(Error, "set_jarl_within", obj, "within %q: no such zone, and not an address or network", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validDNSName reports whether n is a name a Jarl set can look up: LDH
|
||||
// labels (and underscores), no wildcard, without the final dot.
|
||||
func validDNSName(n string) bool {
|
||||
if n == "" || len(n) > 253 {
|
||||
return false
|
||||
}
|
||||
for _, l := range strings.Split(n, ".") {
|
||||
if !dnsLabel.MatchString(l) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *checker) services() {
|
||||
seen := map[string]string{}
|
||||
for _, s := range c.st.Services {
|
||||
|
||||
@@ -29,6 +29,19 @@ func base() *model.State {
|
||||
}
|
||||
}
|
||||
|
||||
func jarlSource() *model.JarlSource {
|
||||
return &model.JarlSource{Names: []model.JarlName{{Name: "nas.example.com", View: "internal"}}, Within: []string{"lan", "10.9.0.0/16"}}
|
||||
}
|
||||
|
||||
func TestJarlSetValid(t *testing.T) {
|
||||
st := base()
|
||||
st.Sets[0].Entries, st.Sets[0].Jarl = nil, jarlSource()
|
||||
st.Sets[0].Jarl.Names = append(st.Sets[0].Jarl.Names, model.JarlName{Name: "_svc.files.example.com"})
|
||||
if ps := Check(st, nil); len(ps) > 0 {
|
||||
t.Fatalf("problems: %+v", ps)
|
||||
}
|
||||
}
|
||||
|
||||
func codes(ps []Problem) string {
|
||||
var out []string
|
||||
for _, p := range ps {
|
||||
@@ -77,6 +90,33 @@ func TestCodes(t *testing.T) {
|
||||
}},
|
||||
{"set_timeout", func(st *model.State) { st.Sets[0].Timeout = 60 }},
|
||||
{"set_dynamic_entries", func(st *model.State) { st.Sets[0].Dynamic = true }},
|
||||
{"set_jarl_entries", func(st *model.State) { st.Sets[0].Jarl = jarlSource() }},
|
||||
{"set_jarl_dynamic", func(st *model.State) {
|
||||
st.Sets[0].Entries, st.Sets[0].Dynamic, st.Sets[0].Jarl = nil, true, jarlSource()
|
||||
}},
|
||||
{"set_jarl_names", func(st *model.State) {
|
||||
st.Sets[0].Entries, st.Sets[0].Jarl = nil, &model.JarlSource{Within: []string{"lan"}}
|
||||
}},
|
||||
{"set_jarl_names", func(st *model.State) {
|
||||
st.Sets[0].Entries, st.Sets[0].Jarl = nil, jarlSource()
|
||||
st.Sets[0].Jarl.Names[0].Name = "*.example.com"
|
||||
}},
|
||||
{"set_jarl_names", func(st *model.State) {
|
||||
st.Sets[0].Entries, st.Sets[0].Jarl = nil, jarlSource()
|
||||
st.Sets[0].Jarl.Names = append(st.Sets[0].Jarl.Names, st.Sets[0].Jarl.Names[0])
|
||||
}},
|
||||
{"set_jarl_within", func(st *model.State) {
|
||||
st.Sets[0].Entries, st.Sets[0].Jarl = nil, jarlSource()
|
||||
st.Sets[0].Jarl.Within = nil
|
||||
}},
|
||||
{"set_jarl_within", func(st *model.State) {
|
||||
st.Sets[0].Entries, st.Sets[0].Jarl = nil, jarlSource()
|
||||
st.Sets[0].Jarl.Within = []string{"dmz"}
|
||||
}},
|
||||
{"set_jarl_anywhere", func(st *model.State) {
|
||||
st.Sets[0].Entries, st.Sets[0].Jarl = nil, jarlSource()
|
||||
st.Sets[0].Jarl.Within = []string{"::/0"}
|
||||
}},
|
||||
{"service_empty", func(st *model.State) { st.Services[0].Entries = nil }},
|
||||
{"service_entry", func(st *model.State) { st.Services[0].Entries = []string{"tcp/99999"} }},
|
||||
{"unknown_zone", func(st *model.State) { st.Rules[1].InZones = []string{"dmz"} }},
|
||||
|
||||
Reference in new issue
Block a user