Skip to content

Commit

Permalink
feat: netstat in API and client
Browse files Browse the repository at this point in the history
Implements netstat in Talos API and client (talosctl).

Signed-off-by: Nico Berlee <nico.berlee@on2it.net>
Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
  • Loading branch information
nberlee authored and smira committed Mar 9, 2023
1 parent fda6da6 commit 97048f7
Show file tree
Hide file tree
Showing 15 changed files with 11,798 additions and 8,266 deletions.
79 changes: 79 additions & 0 deletions api/machine/machine.proto
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ service MachineService {
rpc GenerateClientConfiguration(GenerateClientConfigurationRequest) returns (GenerateClientConfigurationResponse);
// PacketCapture performs packet capture and streams back pcap file.
rpc PacketCapture(PacketCaptureRequest) returns (stream common.Data);
rpc Netstat(NetstatRequest) returns (NetstatResponse);
}

// rpc applyConfiguration
Expand Down Expand Up @@ -1194,3 +1195,81 @@ message BPFInstruction {
uint32 jf = 3;
uint32 k = 4;
}

message NetstatRequest {
enum Filter {
ALL = 0;
CONNECTED = 1;
LISTENING = 2;
}
Filter filter = 1;
message Feature {
bool pid = 1;
}
Feature feature = 2;
message L4proto {
bool tcp = 1;
bool tcp6 = 2;
bool udp = 3;
bool udp6 = 4;
bool udplite = 5;
bool udplite6 = 6;
bool raw = 7;
bool raw6 = 8;
}
L4proto l4proto = 3;
}

message ConnectRecord {
string l4proto = 1;
string localip = 2;
uint32 localport = 3;
string remoteip = 4;
uint32 remoteport = 5;
enum State {
RESERVED = 0;
ESTABLISHED = 1;
SYN_SENT = 2;
SYN_RECV = 3;
FIN_WAIT1 = 4;
FIN_WAIT2 = 5;
TIME_WAIT = 6;
CLOSE = 7;
CLOSEWAIT = 8;
LASTACK = 9;
LISTEN = 10;
CLOSING = 11;
}
State state = 6;
uint64 txqueue = 7;
uint64 rxqueue = 8;
enum TimerActive {
OFF = 0;
ON = 1;
KEEPALIVE = 2;
TIMEWAIT = 3;
PROBE = 4;
}
TimerActive tr = 9;
uint64 timerwhen = 10;
uint64 retrnsmt = 11;
uint32 uid = 12;
uint64 timeout = 13;
uint64 inode = 14;
uint64 ref = 15;
uint64 pointer = 16;
message Process {
uint32 pid = 1;
string name = 2;
}
Process process = 17;
}

message Netstat {
common.Metadata metadata = 1;
repeated ConnectRecord connectrecord = 2;
}

message NetstatResponse {
repeated Netstat messages = 1;
}
255 changes: 255 additions & 0 deletions cmd/talosctl/cmd/talos/netstat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package talos

import (
"context"
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"

"github.com/spf13/cobra"

"github.com/siderolabs/talos/pkg/cli"
"github.com/siderolabs/talos/pkg/machinery/api/machine"
"github.com/siderolabs/talos/pkg/machinery/client"
)

var netstatCmdFlags struct {
verbose bool
extend bool
pid bool
timers bool
listening bool
all bool
tcp bool
udp bool
udplite bool
raw bool
ipv4 bool
ipv6 bool
}

// netstatCmd represents the ls command.
var netstatCmd = &cobra.Command{
Use: "netstat",
Aliases: []string{"ss"},
Short: "Retrieve a socket listing of connections",
Long: ``,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return WithClient(func(ctx context.Context, c *client.Client) error {
req := netstatFlagsToRequest()
response, err := c.Netstat(ctx, req)
if err != nil {
if response == nil {
return fmt.Errorf("error getting netstat: %w", err)
}

cli.Warning("%s", err)
}

err = printNetstat(response)

return err
})
},
}

//nolint:gocyclo
func netstatFlagsToRequest() *machine.NetstatRequest {
req := machine.NetstatRequest{
Feature: &machine.NetstatRequest_Feature{
Pid: netstatCmdFlags.pid,
},
L4Proto: &machine.NetstatRequest_L4Proto{
Tcp: netstatCmdFlags.tcp,
Tcp6: netstatCmdFlags.tcp,
Udp: netstatCmdFlags.udp,
Udp6: netstatCmdFlags.udp,
Udplite: netstatCmdFlags.udplite,
Udplite6: netstatCmdFlags.udplite,
Raw: netstatCmdFlags.raw,
Raw6: netstatCmdFlags.raw,
},
}

switch {
case netstatCmdFlags.all:
req.Filter = machine.NetstatRequest_ALL
case netstatCmdFlags.listening:
req.Filter = machine.NetstatRequest_LISTENING
default:
req.Filter = machine.NetstatRequest_CONNECTED
}

if netstatCmdFlags.verbose {
req.L4Proto.Tcp = true
req.L4Proto.Tcp6 = true
req.L4Proto.Udp = true
req.L4Proto.Udp6 = true
req.L4Proto.Udplite = true
req.L4Proto.Udplite6 = true
req.L4Proto.Raw = true
req.L4Proto.Raw6 = true
}

if !req.L4Proto.Tcp && !req.L4Proto.Tcp6 && !req.L4Proto.Udp && !req.L4Proto.Udp6 && !req.L4Proto.Udplite && !req.L4Proto.Udplite6 && !req.L4Proto.Raw && !req.L4Proto.Raw6 {
req.L4Proto.Tcp = true
req.L4Proto.Tcp6 = true
req.L4Proto.Udp = true
req.L4Proto.Udp6 = true
}

if netstatCmdFlags.ipv4 && !netstatCmdFlags.ipv6 {
req.L4Proto.Tcp6 = false
req.L4Proto.Udp6 = false
req.L4Proto.Udplite6 = false
req.L4Proto.Raw6 = false
}

if netstatCmdFlags.ipv6 && !netstatCmdFlags.ipv4 {
req.L4Proto.Tcp = false
req.L4Proto.Udp = false
req.L4Proto.Udplite = false
req.L4Proto.Raw = false
}

return &req
}

//nolint:gocyclo
func printNetstat(response *machine.NetstatResponse) error {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
node := ""

labels := strings.Join(
[]string{
"Proto",
"Recv-Q",
"Send-Q",
"Local Address",
"Foreign Address",
"State",
}, "\t")

if netstatCmdFlags.extend {
labels += "\t" + strings.Join(
[]string{
"Uid",
"Inode",
}, "\t")
}

if netstatCmdFlags.pid {
labels += "\t" + "PID/Program name"
}

if netstatCmdFlags.timers {
labels += "\t" + "Timer"
}

for i, message := range response.Messages {
if message.Metadata != nil && message.Metadata.Hostname != "" {
node = message.Metadata.Hostname
}

if len(message.Connectrecord) == 0 {
continue
}

for j, record := range message.Connectrecord {
if i == 0 && j == 0 {
if node != "" {
fmt.Fprintln(w, "NODE\t"+labels)
} else {
fmt.Fprintln(w, labels)
}
}

args := []interface{}{}

if node != "" {
args = append(args, node)
}

state := ""
if record.State != 7 {
state = record.State.String()
}

args = append(args, []interface{}{
record.L4Proto,
strconv.FormatUint(record.Rxqueue, 10),
strconv.FormatUint(record.Txqueue, 10),
fmt.Sprintf("%s:%d", record.Localip, record.Localport),
fmt.Sprintf("%s:%s", record.Remoteip, wildcardIfZero(record.Remoteport)),
state,
}...)

if netstatCmdFlags.extend {
args = append(args, []interface{}{
strconv.FormatUint(uint64(record.Uid), 10),
strconv.FormatUint(record.Inode, 10),
}...)
}

if netstatCmdFlags.pid {
if record.Process.Pid != 0 {
args = append(args, []interface{}{
fmt.Sprintf("%d/%s", record.Process.Pid, record.Process.Name),
}...)
} else {
args = append(args, []interface{}{
"-",
}...)
}
}

if netstatCmdFlags.timers {
timerwhen := strconv.FormatFloat(float64(record.Timerwhen)/100, 'f', 2, 64)

args = append(args, []interface{}{
fmt.Sprintf("%s (%s/%d/%d)", strings.ToLower(record.Tr.String()), timerwhen, record.Retrnsmt, record.Timeout),
}...)
}

pattern := strings.Repeat("%s\t", len(args))
pattern = strings.TrimSpace(pattern) + "\n"

fmt.Fprintf(w, pattern, args...)
}
}

return w.Flush()
}

func wildcardIfZero(num uint32) string {
if num == 0 {
return "*"
}

return strconv.FormatUint(uint64(num), 10)
}

func init() {
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.verbose, "verbose", "v", false, "display sockets of all supported transport protocols")
// extend is normally -e but cannot be used as this is endpoint in talosctl
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.extend, "extend", "x", false, "show detailed socket information")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.pid, "programs", "p", false, "show process using socket")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.timers, "timers", "o", false, "display timers")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.listening, "listening", "l", false, "display listening server sockets")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.all, "all", "a", false, "display all sockets states (default: connected)")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.tcp, "tcp", "t", false, "display only TCP sockets")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.udp, "udp", "u", false, "display only UDP sockets")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.udplite, "udplite", "U", false, "display only UDPLite sockets")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.raw, "raw", "w", false, "display only RAW sockets")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.ipv4, "ipv4", "4", false, "display only ipv4 sockets")
netstatCmd.Flags().BoolVarP(&netstatCmdFlags.ipv4, "ipv6", "6", false, "display only ipv6 sockets")

addCommand(netstatCmd)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ require (
github.com/mdlayher/genetlink v1.3.1
github.com/mdlayher/netlink v1.7.1
github.com/mdlayher/netx v0.0.0-20220422152302-c711c2f8512f
github.com/nberlee/go-netstat v0.0.0-20230306184515-0b1bbade8369
github.com/opencontainers/image-spec v1.1.0-rc2
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417
github.com/packethost/packngo v0.29.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nberlee/go-netstat v0.0.0-20230306184515-0b1bbade8369 h1:0impfFsYpeJ96ao5NPNlyjwlQ/YNHhBY3oxriCSHyZA=
github.com/nberlee/go-netstat v0.0.0-20230306184515-0b1bbade8369/go.mod h1:GvDCRLsUKMRN1wULkt7tpnDmjSIE6YGf5zeVq+mBO64=
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d h1:x3S6kxmy49zXVVyhcnrFqxvNVCBPb2KZ9hV2RBdS840=
Expand Down
9 changes: 8 additions & 1 deletion hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,17 @@ rebooting, shutting down a node, accessing packet capture, etcd alarm APIs, etcd
"""

[notes.containers-output]
title = "Talosctl containers"
title = "talosctl containers"
description="""\
`talosctl logs -k` and `talosctl containers -k` now support and output container display names with their ids.
This allows to distinguish between containers with the same name.
"""

[notes.netstat]
title = "talosctl netstat"
description="""\
Talos API was extended to support retrieving a list of network connections (sockets) from the node.
`talosctl netstat` command was added to retrieve the list of network connections.
"""

[make_deps]
Expand Down

0 comments on commit 97048f7

Please sign in to comment.