Skip to content

Commit 3a11c45

Browse files
committed
cli: rework me status and gateway status output
me status: merge each service's status and address into one row (VPN, DNS, SOCKS5), collapse exit-peer details into a single "<ip>, ping <N>ms, direct/via relay" line, regroup rows, reformat uptime ("1d 21h 13m"), and disable table wrapping. gateway status: humanize booleans (enabled/disabled, [connected], direct/via relay), show the full peer id, fall back to id when the name is unknown, server before client, aligned as plain key: value lines.
1 parent 3c0bf20 commit 3a11c45

4 files changed

Lines changed: 237 additions & 40 deletions

File tree

cli/gateway.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,26 @@ func gatewayStatus(api *apiclient.Client, w io.Writer) error {
1515
}
1616
gw := info.VPNGateway
1717

18-
fmt.Fprintf(w, "VPN gateway client enabled: %v\n", gw.ClientEnabled)
19-
fmt.Fprintf(w, "VPN gateway server enabled: %v\n", gw.ServerEnabled)
18+
line := func(label, value string) {
19+
fmt.Fprintf(w, "%-14s %s\n", label+":", value)
20+
}
21+
22+
line("Server", formatEnabled(gw.ServerEnabled))
23+
line("Client", formatEnabled(gw.ClientEnabled))
2024
if gw.ClientEnabled {
21-
fmt.Fprintf(w, "Gateway peer: %s (%s)\n", gw.GatewayPeerName, gw.GatewayPeerID)
22-
fmt.Fprintf(w, "Gateway peer connected: %v\n", gw.Connected)
25+
name := gw.GatewayPeerName
26+
if name == "" {
27+
name = gw.GatewayPeerID
28+
}
29+
line("Gateway peer", fmt.Sprintf("%s [%s]", name, formatConnected(gw.Connected)))
30+
line("Peer ID", gw.GatewayPeerID)
2331
if gw.GatewayPublicIP != "" {
24-
fmt.Fprintf(w, "Gateway public IP: %s\n", gw.GatewayPublicIP)
32+
line("Public IP", gw.GatewayPublicIP)
2533
}
2634
if gw.GatewayPing > 0 {
27-
fmt.Fprintf(w, "Gateway ping: %s\n", gw.GatewayPing.Round(time.Millisecond))
35+
line("Ping", gw.GatewayPing.Round(time.Millisecond).String())
2836
}
29-
fmt.Fprintf(w, "Gateway via relay: %v\n", gw.GatewayThroughRelay)
37+
line("Connection", formatRelay(gw.GatewayThroughRelay))
3038
}
3139

3240
return nil

cli/me.go

Lines changed: 141 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -20,64 +20,180 @@ func printStatus(api *apiclient.Client, w io.Writer) error {
2020
}
2121

2222
rows := [][]string{
23+
{"Name", stats.Name},
2324
{"Download rate", fmt.Sprintf("%s (%s)", stats.NetworkStatsInIECUnits.RateIn, stats.NetworkStatsInIECUnits.TotalIn)},
2425
{"Upload rate", fmt.Sprintf("%s (%s)", stats.NetworkStatsInIECUnits.RateOut, stats.NetworkStatsInIECUnits.TotalOut)},
2526
{"Bootstrap peers", fmt.Sprintf("%d/%d", stats.ConnectedBootstrapPeers, stats.TotalBootstrapPeers)},
26-
{"DNS", formatWorkingStatus(stats.IsAwlDNSSetAsSystem)},
27-
{"SOCKS5 Proxy", formatWorkingStatus(stats.SOCKS5.ListenerEnabled)},
28-
{"SOCKS5 Proxy address", stats.SOCKS5.ListenAddress},
29-
{"SOCKS5 Proxy exit node", stats.SOCKS5.UsingPeerName},
27+
{"VPN", formatVPNStatus(stats.VPN)},
3028
{"VPN gateway client", formatVPNGatewayClient(stats.VPNGateway)},
3129
}
3230
gw := stats.VPNGateway
3331
if gw.ClientEnabled {
34-
if gw.GatewayPublicIP != "" {
35-
rows = append(rows, []string{"VPN gateway public IP", gw.GatewayPublicIP})
32+
if detail := formatExitDetail(gw.GatewayPublicIP, gw.GatewayPing, gw.Connected, gw.GatewayThroughRelay); detail != "" {
33+
rows = append(rows, []string{"VPN gateway exit", detail})
3634
}
37-
if gw.GatewayPing > 0 {
38-
rows = append(rows, []string{"VPN gateway ping", gw.GatewayPing.Round(time.Millisecond).String()})
39-
}
40-
rows = append(rows, []string{"VPN gateway via relay", fmt.Sprintf("%v", gw.GatewayThroughRelay)})
4135
}
4236
rows = append(rows,
4337
[]string{"VPN gateway server", formatWorkingStatus(stats.VPNGateway.ServerEnabled)},
38+
[]string{"SOCKS5 Proxy", formatServiceStatus(stats.SOCKS5.ListenerEnabled, stats.SOCKS5.ListenAddress)},
39+
)
40+
41+
s5 := stats.SOCKS5
42+
if exit := formatSOCKS5ExitNode(s5); exit != "off" {
43+
rows = append(rows, []string{"SOCKS5 exit node", exit})
44+
if detail := formatExitDetail(s5.UsingPeerPublicIP, s5.UsingPeerPing, s5.Connected, s5.UsingPeerThroughRelay); detail != "" {
45+
rows = append(rows, []string{"SOCKS5 exit", detail})
46+
}
47+
}
48+
49+
rows = append(rows,
50+
[]string{"DNS", formatServiceStatus(stats.IsAwlDNSSetAsSystem, stats.AwlDNSAddress)},
4451
[]string{"Reachability", strings.ToLower(stats.Reachability)},
45-
[]string{"Uptime", stats.Uptime.Round(time.Second).String()},
52+
[]string{"Uptime", formatUptime(stats.Uptime)},
4653
[]string{"Server version", stats.ServerVersion},
4754
)
4855

4956
table := tablewriter.NewWriter(w)
57+
table.SetAutoWrapText(false)
5058
table.AppendBulk(rows)
5159

5260
table.Render()
5361

5462
return nil
5563
}
5664

65+
const (
66+
statusWorking = "working"
67+
statusNotWorking = "not working"
68+
)
69+
5770
func formatWorkingStatus(working bool) string {
5871
if working {
59-
return "working"
72+
return statusWorking
73+
}
74+
return statusNotWorking
75+
}
76+
77+
// formatEnabled renders an on/off feature toggle.
78+
func formatEnabled(enabled bool) string {
79+
if enabled {
80+
return "enabled"
81+
}
82+
return "disabled"
83+
}
84+
85+
// formatConnected renders libp2p connectivity to a peer.
86+
func formatConnected(connected bool) string {
87+
if connected {
88+
return "connected"
89+
}
90+
return "disconnected"
91+
}
92+
93+
// formatServiceStatus renders an enabled/disabled service and, when enabled,
94+
// appends its listen address in parentheses, e.g. "working (127.0.0.66:53)".
95+
// The address is omitted while the service is off.
96+
func formatServiceStatus(enabled bool, addr string) string {
97+
if !enabled {
98+
return statusNotWorking
99+
}
100+
if addr != "" {
101+
return fmt.Sprintf("%s (%s)", statusWorking, addr)
102+
}
103+
return statusWorking
104+
}
105+
106+
// formatVPNStatus renders the local VPN interface state, including the
107+
// interface name and assigned address when it is up, e.g.
108+
// "working (awl0, 10.66.0.1/24)".
109+
func formatVPNStatus(vpn entity.VPNInfo) string {
110+
if !vpn.VPNInterfaceEnabled {
111+
return statusNotWorking
60112
}
61-
return "not working"
113+
var parts []string
114+
if vpn.InterfaceName != "" {
115+
parts = append(parts, vpn.InterfaceName)
116+
}
117+
if vpn.IPNet != "" {
118+
parts = append(parts, vpn.IPNet)
119+
}
120+
if len(parts) == 0 {
121+
return statusWorking
122+
}
123+
return fmt.Sprintf("%s (%s)", statusWorking, strings.Join(parts, ", "))
124+
}
125+
126+
// formatExitDetail renders a compact one-line summary of a selected exit peer
127+
// (SOCKS5 or VPN gateway): "<public IP>, ping <N>ms, direct/via relay". Each
128+
// part is included only when meaningful; the relay part appears only while
129+
// connected. Returns "" when nothing is known.
130+
func formatExitDetail(publicIP string, ping time.Duration, connected, throughRelay bool) string {
131+
var parts []string
132+
if publicIP != "" {
133+
parts = append(parts, publicIP)
134+
}
135+
if ping > 0 {
136+
parts = append(parts, "ping "+ping.Round(time.Millisecond).String())
137+
}
138+
if connected {
139+
parts = append(parts, formatRelay(throughRelay))
140+
}
141+
return strings.Join(parts, ", ")
142+
}
143+
144+
// formatRelay renders the connection path of an exit peer in human terms.
145+
func formatRelay(throughRelay bool) string {
146+
if throughRelay {
147+
return "via relay"
148+
}
149+
return "direct"
150+
}
151+
152+
// formatUptime renders uptime with spaces between units. For 24h or more it
153+
// switches to day granularity and drops seconds (e.g. "1d 21h 13m"); below 24h
154+
// it keeps seconds and omits leading zero units (e.g. "1h 25m 5s", "5s").
155+
func formatUptime(d time.Duration) string {
156+
d = d.Round(time.Second)
157+
days := d / (24 * time.Hour)
158+
d -= days * 24 * time.Hour
159+
hours := d / time.Hour
160+
d -= hours * time.Hour
161+
mins := d / time.Minute
162+
d -= mins * time.Minute
163+
secs := d / time.Second
164+
165+
if days > 0 {
166+
return fmt.Sprintf("%dd %dh %dm", int64(days), int64(hours), int64(mins))
167+
}
168+
169+
var parts []string
170+
if hours > 0 {
171+
parts = append(parts, fmt.Sprintf("%dh", int64(hours)))
172+
}
173+
if mins > 0 || hours > 0 {
174+
parts = append(parts, fmt.Sprintf("%dm", int64(mins)))
175+
}
176+
parts = append(parts, fmt.Sprintf("%ds", int64(secs)))
177+
return strings.Join(parts, " ")
178+
}
179+
180+
func formatSOCKS5ExitNode(s5 entity.SOCKS5Info) string {
181+
return formatExitClient(s5.UsingPeerID != "", s5.Connected, s5.UsingPeerName, s5.UsingPeerID)
62182
}
63183

64-
// formatVPNGatewayClient renders the client-side VPN gateway state in a
65-
// single line: either "off", or the gateway peer's name + connectivity.
66-
// Mirrors how SOCKS5 status splits across two rows but compresses to one
67-
// since VPN gateway has fewer knobs to surface.
68184
func formatVPNGatewayClient(gw entity.VPNGatewayInfo) string {
69-
if !gw.ClientEnabled {
185+
return formatExitClient(gw.ClientEnabled, gw.Connected, gw.GatewayPeerName, gw.GatewayPeerID)
186+
}
187+
188+
func formatExitClient(enabled, connected bool, peerName, peerID string) string {
189+
if !enabled {
70190
return "off"
71191
}
72-
name := gw.GatewayPeerName
192+
name := peerName
73193
if name == "" {
74-
name = gw.GatewayPeerID
75-
}
76-
conn := "disconnected"
77-
if gw.Connected {
78-
conn = "connected"
194+
name = peerID
79195
}
80-
return fmt.Sprintf("via %s [%s]", name, conn)
196+
return fmt.Sprintf("%s [%s]", name, formatConnected(connected))
81197
}
82198

83199
func printPeerId(api *apiclient.Client, w io.Writer) error {

cli/me_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/anywherelan/awl/entity"
10+
)
11+
12+
func TestFormatUptime(t *testing.T) {
13+
cases := []struct {
14+
name string
15+
d time.Duration
16+
want string
17+
}{
18+
{"seconds only", 5 * time.Second, "5s"},
19+
{"minutes and seconds", 25*time.Minute + 5*time.Second, "25m 5s"},
20+
{"hours minutes seconds", time.Hour + 25*time.Minute + 5*time.Second, "1h 25m 5s"},
21+
{"zero minutes kept under a day", time.Hour + 5*time.Second, "1h 0m 5s"},
22+
{"just under a day keeps seconds", 23*time.Hour + 59*time.Minute + 59*time.Second, "23h 59m 59s"},
23+
{"exactly a day drops seconds", 24 * time.Hour, "1d 0h 0m"},
24+
{"over a day drops seconds", 45*time.Hour + 13*time.Minute + 31*time.Second, "1d 21h 13m"},
25+
{"sub-second rounds down", 900 * time.Millisecond, "1s"},
26+
}
27+
for _, tc := range cases {
28+
t.Run(tc.name, func(t *testing.T) {
29+
require.Equal(t, tc.want, formatUptime(tc.d))
30+
})
31+
}
32+
}
33+
34+
func TestFormatExitDetail(t *testing.T) {
35+
cases := []struct {
36+
name string
37+
publicIP string
38+
ping time.Duration
39+
connected bool
40+
throughRelay bool
41+
want string
42+
}{
43+
{"full direct", "1.2.3.4", 674 * time.Millisecond, true, false, "1.2.3.4, ping 674ms, direct"},
44+
{"full via relay", "1.2.3.4", 405 * time.Millisecond, true, true, "1.2.3.4, ping 405ms, via relay"},
45+
{"no public ip", "", 100 * time.Millisecond, true, false, "ping 100ms, direct"},
46+
{"zero ping dropped", "1.2.3.4", 0, true, false, "1.2.3.4, direct"},
47+
{"disconnected drops relay", "1.2.3.4", 100 * time.Millisecond, false, false, "1.2.3.4, ping 100ms"},
48+
{"nothing known", "", 0, false, false, ""},
49+
}
50+
for _, tc := range cases {
51+
t.Run(tc.name, func(t *testing.T) {
52+
require.Equal(t, tc.want, formatExitDetail(tc.publicIP, tc.ping, tc.connected, tc.throughRelay))
53+
})
54+
}
55+
}
56+
57+
func TestFormatVPNStatus(t *testing.T) {
58+
require.Equal(t, "not working", formatVPNStatus(entity.VPNInfo{}))
59+
require.Equal(t, "working (awl0, 10.66.0.1/24)", formatVPNStatus(entity.VPNInfo{
60+
VPNInterfaceEnabled: true, InterfaceName: "awl0", IPNet: "10.66.0.1/24",
61+
}))
62+
require.Equal(t, "working (awl0)", formatVPNStatus(entity.VPNInfo{
63+
VPNInterfaceEnabled: true, InterfaceName: "awl0",
64+
}))
65+
require.Equal(t, "working", formatVPNStatus(entity.VPNInfo{VPNInterfaceEnabled: true}))
66+
}
67+
68+
func TestFormatServiceStatus(t *testing.T) {
69+
require.Equal(t, "not working", formatServiceStatus(false, "127.0.0.66:53"))
70+
require.Equal(t, "working (127.0.0.66:53)", formatServiceStatus(true, "127.0.0.66:53"))
71+
require.Equal(t, "working", formatServiceStatus(true, ""))
72+
}

cli_test.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ func TestCLI_Me(t *testing.T) {
4040
require.NoError(t, err)
4141
// Row labels are static; values are dynamic (uptime, bootstrap peers, reachability)
4242
for _, label := range []string{
43-
"Download rate", "Upload rate", "Bootstrap peers",
44-
"DNS", "SOCKS5 Proxy", "SOCKS5 Proxy address",
45-
"SOCKS5 Proxy exit node",
43+
"Name", "Download rate", "Upload rate", "Bootstrap peers",
44+
"VPN", "DNS", "SOCKS5 Proxy",
4645
"VPN gateway client", "VPN gateway server",
4746
"Reachability", "Uptime", "Server version",
4847
} {
@@ -388,11 +387,11 @@ func TestCLI_Gateway(t *testing.T) {
388387
t.Run("StatusEnabled", func(t *testing.T) {
389388
out, err := runCLI(ts, client, "gateway", "status")
390389
require.NoError(t, err)
391-
require.Contains(t, out, "VPN gateway client enabled: true")
390+
require.Regexp(t, `Client:\s+enabled`, out)
391+
require.Regexp(t, `Server:\s+disabled`, out)
392392
require.Contains(t, out, "Gateway peer:")
393393
require.Contains(t, out, exitNode.PeerID())
394-
require.Contains(t, out, "Gateway via relay:")
395-
require.Contains(t, out, "VPN gateway server enabled: false")
394+
require.Regexp(t, `Connection:\s+(direct|via relay)`, out)
396395
})
397396

398397
t.Run("List", func(t *testing.T) {
@@ -428,9 +427,11 @@ func TestCLI_Gateway(t *testing.T) {
428427
statusOut, err := runCLI(ts, client, "me", "status")
429428
require.NoError(t, err)
430429
require.Contains(t, statusOut, "VPN gateway client")
431-
require.Contains(t, statusOut, "via ")
430+
require.Contains(t, statusOut, "peer_2")
432431
require.Contains(t, statusOut, "[connected]")
433-
require.Contains(t, statusOut, "VPN gateway via relay")
432+
// The gateway detail row shows the connection path once connected.
433+
require.True(t, strings.Contains(statusOut, "direct") || strings.Contains(statusOut, "via relay"),
434+
"gateway detail row should show connection path")
434435
})
435436

436437
t.Run("ServerDisable", func(t *testing.T) {

0 commit comments

Comments
 (0)