mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
120 lines
3.4 KiB
Go
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))
|
|
}
|