From 2b2b341abf0e03b51dca319c29a45b8502c0dece Mon Sep 17 00:00:00 2001 From: Bryan Richardson Date: Mon, 11 Sep 2023 13:16:13 -0600 Subject: [PATCH] [minitunnel] track tunneled ports for listing and closing (#1509) * [minitunnel] track tunneled ports for listing and closing * Skip logging of expected errors when users delete a tunnel * Add small blurb in help for "cc tunnel" command * Use VM name when listing tunnels in case multiple VMs have same hostname --- cmd/minimega/cc_cli.go | 95 ++++++++++++++++++++++++++++++- internal/minitunnel/forward.go | 71 +++++++++++++++++++++++ internal/minitunnel/minitunnel.go | 76 +++++++++++++++++++------ internal/ron/tunnel.go | 32 +++++++++++ 4 files changed, 255 insertions(+), 19 deletions(-) create mode 100644 internal/minitunnel/forward.go diff --git a/cmd/minimega/cc_cli.go b/cmd/minimega/cc_cli.go index 9a0f3b97a..e7b1636ee 100644 --- a/cmd/minimega/cc_cli.go +++ b/cmd/minimega/cc_cli.go @@ -97,6 +97,11 @@ without arguments displays the existing mounts. Users can use "clear cc mount" to unmount the filesystem of one or all VMs. This should be done before killing or stopping the VM ("clear namespace " will handle this automatically). +"cc tunnel" allows users to tunnel TCP connections to a local port through a VM +to a remote port. The local port will be created on the minimega cluster host +that the tunneling VM is running on. The remote port can be on the same VM or on +a different VM the tunneling VM has network access to. + "cc test-conn" allows users to test network connectivity from a guest to the given IP or domain name and port. The wait timeout should be specified as a Go duration string (e.g. 5s, 1m). If "udp" is used, a "base64 udp packet" that will @@ -141,6 +146,8 @@ For more documentation, see the article "Command and Control API Tutorial".`, "cc ", "cc ", + "cc ", + "cc ", "cc ", "cc ", @@ -236,6 +243,90 @@ func cliCC(ns *Namespace, c *minicli.Command, resp *minicli.Response) error { // tunnel func cliCCTunnel(ns *Namespace, c *minicli.Command, resp *minicli.Response) error { + v := c.StringArgs["vm"] + + if c.BoolArgs["close"] { + vm := ns.FindVM(v) + if vm == nil { + return vmNotFound(v) + } + + id := c.StringArgs["id"] + tid, err := strconv.Atoi(id) + if err != nil { + return fmt.Errorf("invalid format for tunnel ID") + } + + if err := ns.ccServer.CloseForward(vm.GetUUID(), tid); err != nil { + return err + } + + return nil + } + + if c.BoolArgs["list"] { + // map VM name --> VM UUID + vms := make(map[string]string) + + if v == Wildcard { + clients := ns.ccServer.GetClients() + + for _, client := range clients { + vm := ns.FindVM(client.UUID) + if vm == nil { + return vmNotFound(client.UUID) + } + + vms[vm.GetName()] = client.UUID + } + } else { + vm := ns.FindVM(v) + if vm == nil { + return vmNotFound(v) + } + + vms[v] = vm.GetUUID() + } + + var names []string + for name := range vms { + names = append(names, name) + } + + sort.Strings(names) + + resp.Header = []string{"vm", "id", "src port", "dst", "dst port"} + resp.Tabular = [][]string{} + + for _, name := range names { + forwards, err := ns.ccServer.ListForwards(vms[name]) + if err != nil { + return err + } + + var ids []int + for id := range forwards { + ids = append(ids, id) + } + + sort.Ints(ids) + + for _, id := range ids { + tokens := strings.Split(forwards[id], ":") + + resp.Tabular = append(resp.Tabular, []string{ + name, + strconv.Itoa(id), + tokens[0], + tokens[1], + tokens[2], + }) + } + } + + return nil + } + src, err := strconv.Atoi(c.StringArgs["src"]) if err != nil { return fmt.Errorf("non-integer src: %v : %v", c.StringArgs["src"], err) @@ -248,14 +339,14 @@ func cliCCTunnel(ns *Namespace, c *minicli.Command, resp *minicli.Response) erro host := c.StringArgs["host"] - v := c.StringArgs["vm"] - // get the vm uuid vm := ns.FindVM(v) if vm == nil { return vmNotFound(v) } + log.Debug("got vm: %v %v", vm.GetID(), vm.GetName()) + uuid := vm.GetUUID() return ns.ccServer.Forward(uuid, src, host, dst) diff --git a/internal/minitunnel/forward.go b/internal/minitunnel/forward.go new file mode 100644 index 000000000..ca504a440 --- /dev/null +++ b/internal/minitunnel/forward.go @@ -0,0 +1,71 @@ +package minitunnel + +import ( + "fmt" + "net" +) + +type forward struct { + fid int + src int + host string + dst int + + listener net.Listener + connections []net.Conn +} + +func (f *forward) addConnection(c net.Conn) { + f.connections = append(f.connections, c) +} + +func (f *forward) close() { + f.listener.Close() + + for _, conn := range f.connections { + conn.Close() + } +} + +func (f *forward) String() string { + return fmt.Sprintf("%d:%s:%d", f.src, f.host, f.dst) +} + +func (t *Tunnel) newForward(l net.Listener, src int, host string, dst int) *forward { + return &forward{ + fid: <-t.forwardIDs, + src: src, + host: host, + dst: dst, + + listener: l, + } +} + +func (t *Tunnel) ListForwards() map[int]string { + list := make(map[int]string) + + t.sendLock.Lock() + defer t.sendLock.Unlock() + + for i, f := range t.forwards { + list[i] = f.String() + } + + return list +} + +func (t *Tunnel) CloseForward(id int) error { + f, ok := t.forwards[id] + if !ok { + return fmt.Errorf("forwarder with ID %d not found", id) + } + + f.close() + + t.sendLock.Lock() + defer t.sendLock.Unlock() + + delete(t.forwards, f.fid) + return nil +} diff --git a/internal/minitunnel/minitunnel.go b/internal/minitunnel/minitunnel.go index 48d40c000..b4d7e4f66 100644 --- a/internal/minitunnel/minitunnel.go +++ b/internal/minitunnel/minitunnel.go @@ -11,6 +11,7 @@ import ( "io" "math/rand" "net" + "strings" "sync" log "github.com/sandia-minimega/minimega/v2/pkg/minilog" @@ -27,14 +28,16 @@ const ( FORWARD ) -var errClosing = "use of closed network connection" - type Tunnel struct { transport io.ReadWriteCloser // underlying transport - enc *gob.Encoder - dec *gob.Decoder - quit chan bool // tell the message pump to quit - chans chans + + enc *gob.Encoder + dec *gob.Decoder + quit chan bool // tell the message pump to quit + chans chans + + forwardIDs chan int + forwards map[int]*forward sendLock sync.Mutex } @@ -82,12 +85,23 @@ func ListenAndServe(transport io.ReadWriteCloser) error { t := &Tunnel{ transport: transport, - enc: enc, - dec: dec, - quit: make(chan bool), - chans: makeChans(), + + enc: enc, + dec: dec, + quit: make(chan bool), + chans: makeChans(), + + forwardIDs: make(chan int), + forwards: make(map[int]*forward), } + // start a goroutine to generate forward IDs for us + go func() { + for id := 1; ; id++ { + t.forwardIDs <- id + } + }() + go t.mux() return nil } @@ -97,10 +111,14 @@ func ListenAndServe(transport io.ReadWriteCloser) error { func Dial(transport io.ReadWriteCloser) (*Tunnel, error) { t := &Tunnel{ transport: transport, - enc: gob.NewEncoder(transport), - dec: gob.NewDecoder(transport), - quit: make(chan bool), - chans: makeChans(), + + enc: gob.NewEncoder(transport), + dec: gob.NewDecoder(transport), + quit: make(chan bool), + chans: makeChans(), + + forwardIDs: make(chan int), + forwards: make(map[int]*forward), } handshake := &tunnelMessage{ @@ -121,6 +139,13 @@ func Dial(transport io.ReadWriteCloser) (*Tunnel, error) { return nil, fmt.Errorf("did not receive handshake ack: %v", handshake) } + // start a goroutine to generate forward IDs for us + go func() { + for id := 1; ; id++ { + t.forwardIDs <- id + } + }() + // start the message mux go t.mux() @@ -137,6 +162,7 @@ func (t *Tunnel) Forward(source int, host string, dest int) error { if err != nil { return err } + go t.forward(ln, source, host, dest) return nil } @@ -173,18 +199,32 @@ func (t *Tunnel) Reverse(source int, host string, dest int) error { // listen on source port and start new remote connections for every Accept() func (t *Tunnel) forward(ln net.Listener, source int, host string, dest int) { + f := t.newForward(ln, source, host, dest) + + t.sendLock.Lock() + t.forwards[f.fid] = f + t.sendLock.Unlock() + go func() { <-t.quit - ln.Close() + f.close() + + t.sendLock.Lock() + delete(t.forwards, f.fid) + t.sendLock.Unlock() }() for { conn, err := ln.Accept() if err != nil { - log.Errorln(err) + if !strings.Contains(err.Error(), "use of closed network connection") { + log.Errorln(err) + } + return } + f.addConnection(conn) go t.createTunnel(conn, host, dest) } } @@ -264,7 +304,9 @@ func (t *Tunnel) transfer(in chan *tunnelMessage, conn net.Conn, TID int) { } if err != io.EOF { - log.Errorln(err) + if !strings.Contains(err.Error(), "use of closed network connection") { + log.Errorln(err) + } m := &tunnelMessage{ Type: CLOSED, diff --git a/internal/ron/tunnel.go b/internal/ron/tunnel.go index af0033d75..e36323432 100644 --- a/internal/ron/tunnel.go +++ b/internal/ron/tunnel.go @@ -33,6 +33,38 @@ func (s *Server) Forward(uuid string, source int, host string, dest int) error { return c.tunnel.Forward(source, host, dest) } +func (s *Server) ListForwards(uuid string) (map[int]string, error) { + s.clientLock.Lock() + defer s.clientLock.Unlock() + + c, ok := s.clients[uuid] + if !ok { + return nil, fmt.Errorf("no such client: %v", uuid) + } + + if c.tunnel == nil { + return nil, fmt.Errorf("tunnel has not been initialized for %v", uuid) + } + + return c.tunnel.ListForwards(), nil +} + +func (s *Server) CloseForward(uuid string, id int) error { + s.clientLock.Lock() + defer s.clientLock.Unlock() + + c, ok := s.clients[uuid] + if !ok { + return fmt.Errorf("no such client: %v", uuid) + } + + if c.tunnel == nil { + return fmt.Errorf("tunnel has not been initialized for %v", uuid) + } + + return c.tunnel.CloseForward(id) +} + // Reverse creates a reverse tunnel from guest->host. It is possible to have // multiple clients create a reverse tunnel simultaneously. filter allows // specifying which clients to have create the tunnel.