package persist import ( "fmt" "sort" "strings" ) // RulesUnitName is the systemd unit that restores the panel's forwarding rules // at boot (§2.2 of the port forwarding specification). const RulesUnitName = "gre-panel-rules.service" // SysctlPath is the one kernel-parameter file the panel writes. Everything else // under /etc/sysctl.d belongs to the distribution or to another package, and // /etc/sysctl.conf belongs to the operator (§6.3.3). const SysctlPath = "/etc/sysctl.d/99-gre-panel.conf" // PreviousMarker records what a parameter was before the panel changed it, so // the panel can offer to put it back without guessing. It is a comment, so the // file stays a plain sysctl file that sysctl itself reads. const PreviousMarker = "# gre-panel:previous " // RulesUnitOptions describes the commands the restore unit runs. They arrive as // argv slices from the netfilter backend rather than being built here, so the // unit and a live apply can never install different rulesets. type RulesUnitOptions struct { // Backend names the netfilter interface in use, for the description line. Backend string // Remove are tolerated commands run before the restore. On the iptables // backend these take out the panel's own jump rules, so that reinstalling // them cannot leave two: delete-then-insert is how "exactly one" is // expressed without a shell. Remove [][]string // Restore installs the ruleset. Restore [][]string // Install are the commands run after the restore, which is where the jump // rules go back in. Install [][]string // Stop are tolerated commands that remove the panel's rules when the unit // is stopped. They touch the panel's own namespace and nothing else. Stop [][]string } // RulesUnit renders the restore unit. // // It is ordered after the network is up, because the ruleset names addresses // that must exist, and before Docker, because Docker rebuilds its own chains on // start and the panel's rules should already be in place when it does. Ordering // against a unit that is not installed is ignored by systemd, so naming Docker // costs nothing on a host without it. func (r *Renderer) RulesUnit(opts RulesUnitOptions) string { var b strings.Builder b.WriteString(OwnershipMarker + " role=rules\n") b.WriteString("# This file is generated by gre-panel. Edit the forwarding rules in the panel;\n") b.WriteString("# changes made here are overwritten the next time they are applied.\n") b.WriteString("#\n") b.WriteString("# The panel keeps its rules in its own database and renders them to its own\n") b.WriteString("# file. Nothing here snapshots or restores the whole system's ruleset: the\n") b.WriteString("# commands below touch the panel's own table or chains and nothing else.\n") b.WriteString("\n[Unit]\n") fmt.Fprintf(&b, "Description=GRE panel port forwarding rules (%s)\n", orDefault(opts.Backend, "netfilter")) b.WriteString("After=network-online.target\n") b.WriteString("Wants=network-online.target\n") // Docker rebuilds its own chains when it starts; being ordered before it // means the panel's rules are already installed when that happens. b.WriteString("Before=docker.service\n") b.WriteString("\n[Service]\n") b.WriteString("Type=oneshot\n") b.WriteString("RemainAfterExit=yes\n") for _, argv := range opts.Remove { fmt.Fprintf(&b, "ExecStartPre=-%s\n", join(argv)) } for _, argv := range opts.Restore { fmt.Fprintf(&b, "ExecStart=%s\n", join(argv)) } for _, argv := range opts.Install { fmt.Fprintf(&b, "ExecStart=%s\n", join(argv)) } for _, argv := range opts.Stop { fmt.Fprintf(&b, "ExecStop=-%s\n", join(argv)) } // Deliberately no Restart=: this is a Type=oneshot unit, where the directive // does nothing. b.WriteString("\n[Install]\n") b.WriteString("WantedBy=multi-user.target\n") return b.String() } // SysctlValue is one kernel parameter the panel sets, with what it was before. type SysctlValue struct { Key string Value string // Previous is what the parameter held before the panel set it, recorded so // the panel can offer to put it back. Empty means it was already what the // panel wants, or was not read. Previous string } // SysctlFile renders the panel's own sysctl file. // // Recording the previous value in a comment is what makes the revert of §2.3 // possible without a second state store: the file says what the panel changed // and what it changed it from, and it stays a file sysctl can read. func (r *Renderer) SysctlFile(values []SysctlValue) string { sorted := append([]SysctlValue(nil), values...) sort.Slice(sorted, func(i, j int) bool { return sorted[i].Key < sorted[j].Key }) var b strings.Builder b.WriteString(OwnershipMarker + " role=sysctl\n") b.WriteString("# Kernel parameters the port forwarding subsystem needs. This file belongs to\n") b.WriteString("# gre-panel; every other file under /etc/sysctl.d, and /etc/sysctl.conf itself,\n") b.WriteString("# belongs to the distribution or to you, and the panel never writes them.\n") b.WriteString("#\n") b.WriteString("# The panel does not undo these by itself when the last rule is deleted: other\n") b.WriteString("# software on this server may have come to depend on forwarding being on. The\n") b.WriteString("# previous value is recorded below so the panel can offer to put it back.\n") for _, v := range sorted { if v.Previous != "" { fmt.Fprintf(&b, "%s%s=%s\n", PreviousMarker, v.Key, v.Previous) } } for _, v := range sorted { fmt.Fprintf(&b, "%s=%s\n", v.Key, v.Value) } return b.String() } // ParsePreviousValues reads back what a rendered sysctl file recorded, which is // what the revert offer is built from. func ParsePreviousValues(content string) map[string]string { out := map[string]string{} for _, line := range strings.Split(content, "\n") { trimmed := strings.TrimSpace(line) if !strings.HasPrefix(trimmed, PreviousMarker) { continue } key, value, ok := strings.Cut(strings.TrimPrefix(trimmed, PreviousMarker), "=") if !ok { continue } out[strings.TrimSpace(key)] = strings.TrimSpace(value) } return out }