Converge openings through the firewall an adopted node was found with, and retire it only when the node converges (hq ADR 0100)
This commit is contained in:
@@ -78,6 +78,12 @@ const (
|
||||
// cadence. Tools, hooks and event consumers are not separate modes: they are loaded by a tool
|
||||
// host, which is itself a process that stays up.
|
||||
TypeProcess Type = "process"
|
||||
|
||||
// TypeOpening is a port the mesh needs reachable on an adopted node, converged through the
|
||||
// firewall found there in that firewall's own terms (novox/hq ADR 0100). A state, not a
|
||||
// command: the host adds the rule it marks as the mesh's when it is missing, and removes only
|
||||
// what it marked — which is what lets it travel over the link.
|
||||
TypeOpening Type = "opening"
|
||||
)
|
||||
|
||||
// Resource is one thing that should be true of the machine.
|
||||
@@ -603,6 +609,74 @@ func (s *Service) validate(where string, _ bool) []string {
|
||||
return problems
|
||||
}
|
||||
|
||||
// Opening is a port reachable on an adopted node, from where, and on which path.
|
||||
//
|
||||
// **From** is everywhere or mesh — the private network, by its interface. **Path** is incoming,
|
||||
// for something listening on the machine, or forwarded, for a published container port: the found
|
||||
// firewall sees a published port after the runtime has translated it, so a forwarded opening names
|
||||
// the container's own port in To as well as the machine's in Port.
|
||||
type Opening struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"protocol"`
|
||||
From string `json:"from"`
|
||||
Path string `json:"path"`
|
||||
To int `json:"to,omitempty"`
|
||||
}
|
||||
|
||||
// Where an opening admits from, and the path it is on.
|
||||
const (
|
||||
FromEverywhere = "everywhere"
|
||||
FromMesh = "mesh"
|
||||
PathIncoming = "incoming"
|
||||
PathForwarded = "forwarded"
|
||||
)
|
||||
|
||||
func (o *Opening) Identity() string { return o.ID }
|
||||
func (o *Opening) Kind() Type { return TypeOpening }
|
||||
|
||||
func (o *Opening) Target() string {
|
||||
if o.Path == PathForwarded {
|
||||
return fmt.Sprintf("%s/%d forwarded to %d from %s", o.Protocol, o.Port, o.To, o.From)
|
||||
}
|
||||
return fmt.Sprintf("%s/%d %s from %s", o.Protocol, o.Port, o.Path, o.From)
|
||||
}
|
||||
|
||||
func (o *Opening) validate(where string, _ bool) []string {
|
||||
var problems []string
|
||||
if o.Port < 1 || o.Port > 65535 {
|
||||
problems = append(problems, fmt.Sprintf("%s: an opening's port is 1-65535, not %d", where, o.Port))
|
||||
}
|
||||
if o.Protocol != "tcp" && o.Protocol != "udp" {
|
||||
problems = append(problems, fmt.Sprintf("%s: an opening is tcp or udp, not %q", where, o.Protocol))
|
||||
}
|
||||
if o.From != FromEverywhere && o.From != FromMesh {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: an opening is from %q or %q, not %q", where, FromEverywhere, FromMesh, o.From))
|
||||
}
|
||||
switch o.Path {
|
||||
case PathIncoming:
|
||||
if o.To != 0 {
|
||||
problems = append(problems, where+
|
||||
": an incoming opening names no container port; only a forwarded one does")
|
||||
}
|
||||
case PathForwarded:
|
||||
if o.To < 1 || o.To > 65535 {
|
||||
problems = append(problems, where+
|
||||
": a forwarded opening names the container's port it reaches, as to, 1-65535")
|
||||
}
|
||||
default:
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: an opening's path is %q or %q, not %q", where, PathIncoming, PathForwarded, o.Path))
|
||||
}
|
||||
if !strings.HasPrefix(o.ID, AdoptionPrefix) {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: an opening is the mesh's own, so its id starts %q", where, AdoptionPrefix))
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// Package is a package that should be present.
|
||||
//
|
||||
// Present is the whole of what it asserts, never a version: version is the package manager's
|
||||
@@ -810,6 +884,8 @@ func newOf(t Type) Resource {
|
||||
return &Access{}
|
||||
case TypeProcess:
|
||||
return &Process{}
|
||||
case TypeOpening:
|
||||
return &Opening{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -818,7 +894,7 @@ func newOf(t Type) Resource {
|
||||
func Vocabulary() []Type {
|
||||
return []Type{
|
||||
TypeAccess, TypeAction, TypeArchive, TypeContainer, TypeDirectory, TypeFile,
|
||||
TypeNetwork, TypePackage, TypeProcess, TypeService, TypeUser,
|
||||
TypeNetwork, TypeOpening, TypePackage, TypeProcess, TypeService, TypeUser,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1051,6 +1127,18 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
|
||||
d.Resources = append(d.Resources, resource)
|
||||
}
|
||||
problems = append(problems, checkAdoption(env.Adoption, d.Resources, allowActions)...)
|
||||
if env.Adoption == nil {
|
||||
for _, r := range d.Resources {
|
||||
if r.Kind() == TypeOpening {
|
||||
// On a converged node the mesh's own filter admits what is declared, and the
|
||||
// found firewall is retired; an opening there would be a rule in a firewall the
|
||||
// mesh has disabled (novox/hq ADR 0100).
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"resource %q: an opening is for an adopted node, and this declaration does not "+
|
||||
"say the node is adopted", r.Identity()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(problems) > 0 {
|
||||
return nil, &RefusalError{Problems: problems}
|
||||
|
||||
@@ -266,7 +266,7 @@ func TestAFieldTheNewTypesDoNotUseIsRefused(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheVocabularyIsTheElevenShapesTheMeshNeeds(t *testing.T) {
|
||||
func TestTheVocabularyIsTheTwelveShapesTheMeshNeeds(t *testing.T) {
|
||||
// Six of them the bootstrap uses (novox/hq 07-the-foundation.md), and removing one is a
|
||||
// failing test rather than a discovery during a first-node install.
|
||||
//
|
||||
@@ -282,7 +282,7 @@ func TestTheVocabularyIsTheElevenShapesTheMeshNeeds(t *testing.T) {
|
||||
}
|
||||
for _, want := range []Type{
|
||||
TypeDirectory, TypeFile, TypeService, TypePackage, TypeContainer, TypeAction,
|
||||
TypeUser, TypeArchive, TypeNetwork, TypeAccess, TypeProcess,
|
||||
TypeUser, TypeArchive, TypeNetwork, TypeAccess, TypeProcess, TypeOpening,
|
||||
} {
|
||||
if !speaks[want] {
|
||||
t.Errorf("the host no longer speaks %q", want)
|
||||
@@ -309,8 +309,12 @@ func TestTheVocabularyIsTheElevenShapesTheMeshNeeds(t *testing.T) {
|
||||
// It is a full-host shape rather than a portable one: it needs a process supervisor to install
|
||||
// into. It does NOT need a container runtime, which is the point — only software that
|
||||
// genuinely needs isolation asks for a container.
|
||||
if len(speaks) != 11 {
|
||||
t.Errorf("the vocabulary is %d shapes rather than 11; every addition widens what a compromised "+
|
||||
//
|
||||
// `opening` is the twelfth, and novox/hq ADR 0100 is its decision: on an adopted node the
|
||||
// firewall found there stays in force, and what the mesh needs reachable is converged through
|
||||
// it as a state the host marks as the mesh's — never a command, which the link may not carry.
|
||||
if len(speaks) != 12 {
|
||||
t.Errorf("the vocabulary is %d shapes rather than 12; every addition widens what a compromised "+
|
||||
"control plane can express, so a change here is a decision: %s",
|
||||
len(speaks), vocabulary())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user