196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
// SPDX-License-Identifier: MIT
|
|||
|
|
|
||
|
|
package cli
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"github.com/spf13/cobra"
|
||
|
|
|
||
|
|
"prx/internal/config"
|
||
|
|
"prx/internal/engine"
|
||
|
|
"prx/internal/execx"
|
||
|
|
"prx/internal/iprule"
|
||
|
|
"prx/internal/nft"
|
||
|
|
"prx/internal/state"
|
||
|
|
)
|
||
|
|
|
||
|
|
type statusReport struct {
|
||
|
|
Enabled bool `json:"enabled"`
|
||
|
|
WANInterface string `json:"wan_interface,omitempty"`
|
||
|
|
Gateway4 string `json:"gateway_v4,omitempty"`
|
||
|
|
Gateway6 string `json:"gateway_v6,omitempty"`
|
||
|
|
Mark string `json:"mark"`
|
||
|
|
RuleV4 string `json:"ipv4_rule"`
|
||
|
|
TableV4 string `json:"ipv4_table"`
|
||
|
|
RuleV6 string `json:"ipv6_rule"`
|
||
|
|
TableV6 string `json:"ipv6_table"`
|
||
|
|
Engine string `json:"engine"`
|
||
|
|
NftTable string `json:"nft_table"`
|
||
|
|
EnabledAt string `json:"enabled_at,omitempty"`
|
||
|
|
// StateError reports a state file that could not be read, if any.
|
||
|
|
StateError string `json:"state_error,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func (a *app) newStatusCmd() *cobra.Command {
|
||
|
|
var asJSON bool
|
||
|
|
cmd := &cobra.Command{
|
||
|
|
Use: "status",
|
||
|
|
Short: "Show current proxy routing status",
|
||
|
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||
|
|
cfg, err := a.loadConfig()
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
r := execx.OSRunner{}
|
||
|
|
ctx := cmd.Context()
|
||
|
|
|
||
|
|
// A corrupt or unreadable state file must not hide the rest of
|
||
|
|
// the report: show what we know and flag the problem.
|
||
|
|
st, stateErr := state.Load(cfg.State.Path)
|
||
|
|
|
||
|
|
rep := statusReport{
|
||
|
|
Enabled: st.Enabled,
|
||
|
|
WANInterface: firstNonEmptyStr(st.WANInterface, cfg.Network.WANInterface),
|
||
|
|
Gateway4: st.Gateway4,
|
||
|
|
Gateway6: st.Gateway6,
|
||
|
|
Mark: fmt.Sprintf("0x%x", cfg.Marks.InboundMark),
|
||
|
|
RuleV4: ruleStatus(ctx, r, 4, cfg.Marks.InboundMark, cfg.Routing.TableV4, cfg.Routing.PriorityV4),
|
||
|
|
TableV4: tableStatus(ctx, r, 4, cfg.Routing.TableV4),
|
||
|
|
RuleV6: ruleStatus(ctx, r, 6, cfg.Marks.InboundMark, cfg.Routing.TableV6, cfg.Routing.PriorityV6),
|
||
|
|
TableV6: tableStatus(ctx, r, 6, cfg.Routing.TableV6),
|
||
|
|
Engine: engineStatus(ctx, r, cfg),
|
||
|
|
NftTable: nftStatus(ctx, r),
|
||
|
|
StateError: errorText(stateErr),
|
||
|
|
}
|
||
|
|
if !st.EnabledAt.IsZero() {
|
||
|
|
rep.EnabledAt = st.EnabledAt.Format("2006-01-02 15:04:05 MST")
|
||
|
|
}
|
||
|
|
|
||
|
|
if asJSON {
|
||
|
|
enc := json.NewEncoder(a.out)
|
||
|
|
enc.SetIndent("", " ")
|
||
|
|
return enc.Encode(rep)
|
||
|
|
}
|
||
|
|
|
||
|
|
stateStr := "disabled"
|
||
|
|
if rep.Enabled {
|
||
|
|
stateStr = "enabled"
|
||
|
|
}
|
||
|
|
fmt.Fprintf(a.out, "prx: %s\n", stateStr)
|
||
|
|
fmt.Fprintf(a.out, " mark: %s\n", rep.Mark)
|
||
|
|
if rep.WANInterface != "" {
|
||
|
|
fmt.Fprintf(a.out, " interface: %s (gw4=%s gw6=%s)\n", rep.WANInterface, orDash(rep.Gateway4), orDash(rep.Gateway6))
|
||
|
|
}
|
||
|
|
fmt.Fprintf(a.out, " ipv4 rule: %s\n", rep.RuleV4)
|
||
|
|
fmt.Fprintf(a.out, " ipv4 table: %s\n", rep.TableV4)
|
||
|
|
fmt.Fprintf(a.out, " ipv6 rule: %s\n", rep.RuleV6)
|
||
|
|
fmt.Fprintf(a.out, " ipv6 table: %s\n", rep.TableV6)
|
||
|
|
fmt.Fprintf(a.out, " nft table: %s\n", rep.NftTable)
|
||
|
|
fmt.Fprintf(a.out, " engine: %s\n", rep.Engine)
|
||
|
|
if rep.EnabledAt != "" {
|
||
|
|
fmt.Fprintf(a.out, " since: %s\n", rep.EnabledAt)
|
||
|
|
}
|
||
|
|
if rep.StateError != "" {
|
||
|
|
fmt.Fprintf(a.out, " warning: state file unreadable: %s\n", rep.StateError)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
},
|
||
|
|
}
|
||
|
|
cmd.Flags().BoolVar(&asJSON, "json", false, "output JSON")
|
||
|
|
return cmd
|
||
|
|
}
|
||
|
|
|
||
|
|
// errorText returns the error message, or an empty string when err is nil.
|
||
|
|
func errorText(err error) string {
|
||
|
|
if err == nil {
|
||
|
|
return ""
|
||
|
|
}
|
||
|
|
return err.Error()
|
||
|
|
}
|
||
|
|
|
||
|
|
func ruleStatus(ctx context.Context, r execx.Runner, family int, mark uint32, table, prio int) string {
|
||
|
|
spec := iprule.Spec{Family: family, Mark: mark, Table: table, Priority: prio}
|
||
|
|
ok, err := iprule.RuleExists(ctx, r, spec)
|
||
|
|
switch {
|
||
|
|
case err != nil:
|
||
|
|
return "unknown (" + err.Error() + ")"
|
||
|
|
case ok:
|
||
|
|
return fmt.Sprintf("present (fwmark 0x%x -> table %d prio %d)", mark, table, prio)
|
||
|
|
default:
|
||
|
|
return "absent"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func tableStatus(ctx context.Context, r execx.Runner, family, table int) string {
|
||
|
|
ok, err := iprule.TableHasRoutes(ctx, r, family, table)
|
||
|
|
switch {
|
||
|
|
case err != nil:
|
||
|
|
return "unknown (" + err.Error() + ")"
|
||
|
|
case ok:
|
||
|
|
return fmt.Sprintf("table %d populated", table)
|
||
|
|
default:
|
||
|
|
return fmt.Sprintf("table %d empty", table)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func engineStatus(ctx context.Context, r execx.Runner, cfg *config.Config) string {
|
||
|
|
if !cfg.Engine.Manage || cfg.Engine.ServiceName == "" {
|
||
|
|
return "not managed"
|
||
|
|
}
|
||
|
|
active, err := engine.IsActive(ctx, r, cfg.Engine.ServiceName)
|
||
|
|
if err != nil {
|
||
|
|
return "unknown (" + err.Error() + ")"
|
||
|
|
}
|
||
|
|
if active {
|
||
|
|
return cfg.Engine.ServiceName + " active"
|
||
|
|
}
|
||
|
|
return cfg.Engine.ServiceName + " inactive"
|
||
|
|
}
|
||
|
|
|
||
|
|
func nftStatus(ctx context.Context, r execx.Runner) string {
|
||
|
|
ok, err := nft.Exists(ctx, r)
|
||
|
|
switch {
|
||
|
|
case err != nil:
|
||
|
|
if isPermissionError(err) {
|
||
|
|
return nft.Table() + " unknown (needs root)"
|
||
|
|
}
|
||
|
|
return nft.Table() + " unknown (" + err.Error() + ")"
|
||
|
|
case ok:
|
||
|
|
return nft.Table() + " present"
|
||
|
|
default:
|
||
|
|
return nft.Table() + " absent"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// isPermissionError reports whether err was caused by missing privileges,
|
||
|
|
// as opposed to a missing binary or a malformed invocation.
|
||
|
|
func isPermissionError(err error) bool {
|
||
|
|
msg := err.Error()
|
||
|
|
for _, marker := range []string{"Operation not permitted", "Permission denied", "not permitted"} {
|
||
|
|
if strings.Contains(msg, marker) {
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func orDash(s string) string {
|
||
|
|
if s == "" {
|
||
|
|
return "-"
|
||
|
|
}
|
||
|
|
return s
|
||
|
|
}
|
||
|
|
|
||
|
|
func firstNonEmptyStr(vals ...string) string {
|
||
|
|
for _, v := range vals {
|
||
|
|
if v != "" {
|
||
|
|
return v
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return ""
|
||
|
|
}
|