Skip to content

Commit

Permalink
[minitunnel] track tunneled ports for listing and closing (#1509)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
activeshadow authored Sep 11, 2023
1 parent 4e36236 commit 2b2b341
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 19 deletions.
95 changes: 93 additions & 2 deletions cmd/minimega/cc_cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>" 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
Expand Down Expand Up @@ -141,6 +146,8 @@ For more documentation, see the article "Command and Control API Tutorial".`,
"cc <exitcode,> <id> <vm name, hostname, or uuid>",

"cc <tunnel,> <vm name or uuid> <src port> <host> <dst port>",
"cc <tunnel,> <close,> <vm name or uuid> <id>",
"cc <tunnel,> <list,> <vm name, uuid, or all>",
"cc <rtunnel,> <src port> <host> <dst port>",

"cc <delete,> <command,> <id or prefix or all>",
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
71 changes: 71 additions & 0 deletions internal/minitunnel/forward.go
Original file line number Diff line number Diff line change
@@ -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
}
76 changes: 59 additions & 17 deletions internal/minitunnel/minitunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io"
"math/rand"
"net"
"strings"
"sync"

log "github.com/sandia-minimega/minimega/v2/pkg/minilog"
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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{
Expand All @@ -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()

Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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,
Expand Down
32 changes: 32 additions & 0 deletions internal/ron/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 2b2b341

Please sign in to comment.