// 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 "" }