Files
ctrld/cmd/cli/pf_state_darwin.go
T

120 lines
3.4 KiB
Go

package cli
import (
"bytes"
"context"
"errors"
"os/exec"
"time"
)
// pfStateReadTimeout bounds each state read. These reads run on the network change
// path, so a pfctl that waits on a busy kernel lock must not hold it.
const pfStateReadTimeout = 3 * time.Second
// pfStateReadMaxBytes bounds the output of one state read. A ruleset or an
// interface dump of this size is not the state ctrld reads, and these reads run
// on the network change path.
const pfStateReadMaxBytes = 1 << 20
// errPFStateOutputTooLarge marks output that passed the bound. The bytes kept
// are a prefix of the real output, so the caller must not parse them.
var errPFStateOutputTooLarge = errors.New("pf state output passed its bound")
// boundedBuffer keeps at most limit bytes and remembers that it dropped the
// rest.
type boundedBuffer struct {
bytes.Buffer
limit int
exceeded bool
}
func (b *boundedBuffer) Write(p []byte) (int, error) {
room := b.limit - b.Len()
if len(p) > room {
b.exceeded = true
p = p[:max(room, 0)]
}
if _, err := b.Buffer.Write(p); err != nil {
return 0, err
}
return len(p), nil
}
// pfStateRunCommand runs one read-only state command. A var so tests replace it
// instead of shelling out.
var pfStateRunCommand = func(name string, args ...string) ([]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), pfStateReadTimeout)
defer cancel()
out := &boundedBuffer{limit: pfStateReadMaxBytes}
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdout = out
cmd.Stderr = out
err := cmd.Run()
if out.exceeded {
return nil, errPFStateOutputTooLarge
}
return out.Bytes(), err
}
// pfRuleDump carries pfctl rule output that the caller already read. A nil
// field means that the caller holds no bytes for that flag, so the read runs
// here.
type pfRuleDump struct {
rules []byte // "pfctl -sr"
nat []byte // "pfctl -sn"
}
// pfAnchorNames returns the anchor names of the running ruleset. A failed read
// gives no names, because a partial list reads as an anchor that went away.
func (p *prog) pfAnchorNames(dump pfRuleDump) []string {
rules, ok := p.pfRuleBytes(dump.rules, "-sr", "list filter anchors")
if !ok {
return nil
}
nat, ok := p.pfRuleBytes(dump.nat, "-sn", "list translation anchors")
if !ok {
return nil
}
return parsePFAnchorNames(bytes.NewReader(rules), bytes.NewReader(nat))
}
// pfRuleBytes returns the rule output of one pfctl flag. It starts pfctl only
// when the caller holds no bytes, and a failed read feeds the backoff that
// keeps ctrld off an exhausted host.
func (p *prog) pfRuleBytes(held []byte, flag, operation string) ([]byte, bool) {
if held != nil {
return held, true
}
out, err := pfStateRunCommand("pfctl", flag)
if err != nil {
p.pfBackoffResourceExhaustion(err, out, operation)
return nil, false
}
return out, true
}
// pfStatus reports whether pf runs and for how long. A failed read gives false
// and "".
func (p *prog) pfStatus() (bool, string) {
out, err := pfStateRunCommand("pfctl", "-si")
if err != nil {
p.pfBackoffResourceExhaustion(err, out, "read pf status")
return false, ""
}
return parsePFStatus(bytes.NewReader(out))
}
// tunnelOwner returns the network extension that owns a tunnel interface, or ""
// when the read fails or no extension owns it.
func tunnelOwner(iface string) string {
if iface == "" {
return ""
}
out, err := pfStateRunCommand("ifconfig", "-v", iface)
if err != nil {
return ""
}
return parseIfconfigAgent(bytes.NewReader(out))
}