diff --git a/.travis.yml b/.travis.yml index f7c2f1b..6b9e22b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,9 @@ language: go +dist: bionic go: - 1.13.x before_install: +- sudo apt-get -y install python3-pydbus - go get -u github.com/mattn/goveralls - go get -u github.com/golangci/golangci-lint/cmd/golangci-lint script: diff --git a/Makefile b/Makefile index 56bdd91..4d9cfe2 100644 --- a/Makefile +++ b/Makefile @@ -39,7 +39,7 @@ test: $(SLIRPNETSTACKDEP) SLIRPNETSTACKBIN=$(SLIRPNETSTACKBIN) \ PYTHONPATH=. \ PYTHONIOENCODING=utf-8 \ - unshare -Ur python3 -m tests.runner tests + unshare -Ur dbus-run-session --config-file=tests/dbus.conf python3 -m tests.runner tests cover: bin/gocovmerge @-mkdir -p .cover diff --git a/addr.go b/addr.go index 3e90404..5fd5ea5 100644 --- a/addr.go +++ b/addr.go @@ -33,6 +33,7 @@ type FwdAddr struct { kaEnable bool kaInterval time.Duration proxyProtocol bool + listener Listener } type FwdAddrSlice []FwdAddr @@ -49,7 +50,7 @@ func (f *FwdAddrSlice) String() string { return strings.Join(s, " ") } -func (f *FwdAddrSlice) Set(value string) error { +func (fwa *FwdAddr) Set(value string) error { var ( bindPort, bindIP string network string @@ -67,7 +68,6 @@ func (f *FwdAddrSlice) Set(value string) error { rest = p[0] } - var fwa FwdAddr switch network { case "udprpc": fwa.network = "udp" @@ -141,19 +141,32 @@ func (f *FwdAddrSlice) Set(value string) error { fwa.host.Port = uint16(port) } + return nil +} + +func (f *FwdAddrSlice) Set(value string) error { + var fwa FwdAddr + + if err := fwa.Set(value); err != nil { + return err + } + *f = append(*f, fwa) return nil } +func (fa *FwdAddr) SetDefaultAddr(bindAddrDef net.IP, hostAddrDef net.IP) { + if fa.bind.Addr == "" { + fa.bind.Addr = tcpip.Address(bindAddrDef) + } + if fa.host.Addr == "" { + fa.host.Addr = tcpip.Address(hostAddrDef) + } +} + func (f *FwdAddrSlice) SetDefaultAddrs(bindAddrDef net.IP, hostAddrDef net.IP) { for i, _ := range *f { - fa := &(*f)[i] - if fa.bind.Addr == "" { - fa.bind.Addr = tcpip.Address(bindAddrDef) - } - if fa.host.Addr == "" { - fa.host.Addr = tcpip.Address(hostAddrDef) - } + (*f)[i].SetDefaultAddr(bindAddrDef, hostAddrDef) } } diff --git a/dbus.go b/dbus.go new file mode 100644 index 0000000..96bdae7 --- /dev/null +++ b/dbus.go @@ -0,0 +1,215 @@ +package main + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/godbus/dbus/v5" + "github.com/godbus/dbus/v5/introspect" + "gvisor.dev/gvisor/pkg/tcpip" + "gvisor.dev/gvisor/pkg/tcpip/header" + "gvisor.dev/gvisor/pkg/tcpip/stack" + "gvisor.dev/gvisor/pkg/tcpip/transport/tcp" + "gvisor.dev/gvisor/pkg/tcpip/transport/udp" +) + +func getInfo(tcpep tcpip.Endpoint) *stack.TransportEndpointInfo { + switch t := tcpep.Info().(type) { + case *stack.TransportEndpointInfo: + return t + case *tcp.EndpointInfo: + return &t.TransportEndpointInfo + default: + fmt.Fprintf(os.Stderr, "unexpected type %T\n", t) + return nil + } + +} + +func (s *State) GetInfo() (string, *dbus.Error) { + var b strings.Builder + + fmt.Fprintf(&b, "Protocol[State] Source Address Port Dest. Address Port\n") + teps := s.stack.RegisteredEndpoints() + for _, tep := range teps { + ep := tep.(tcpip.Endpoint) + tepi := getInfo(ep) + if tepi == nil { + continue + } + + transName := "unknown" + state := "" + switch tepi.TransProto { + case header.UDPProtocolNumber: + transName = "UDP" + state = udp.EndpointState(ep.State()).String() + case header.TCPProtocolNumber: + transName = "TCP" + state = tcp.EndpointState(ep.State()).String() + } + + protocolState := transName + "[" + state + "]" + fmt.Fprintf(&b, "%-19v %15v %5v %15v %5v\n", + protocolState, + tepi.ID.RemoteAddress, tepi.ID.RemotePort, + tepi.ID.LocalAddress, tepi.ID.LocalPort) + } + return b.String(), nil +} + +type forwardEntry struct { + Network string + Addr string + Port uint16 + HostAddr string + HostPort uint16 +} + +func (s *State) RemoveRemoteForward(addr string) *dbus.Error { + var fwa FwdAddr + + if err := fwa.Set(addr); err != nil { + return dbus.MakeFailedError(err) + } + + fwa.SetDefaultAddr(netParseIP("10.0.2.2"), netParseIP("127.0.0.1")) + + if err := s.removeRemoteFwd(&fwa); err != nil { + return dbus.MakeFailedError(err) + } + + return nil +} + +func (s *State) AddRemoteForward(addr string) *dbus.Error { + var fwa FwdAddr + + if err := fwa.Set(addr); err != nil { + return dbus.MakeFailedError(err) + } + + fwa.SetDefaultAddr(netParseIP("10.0.2.2"), netParseIP("127.0.0.1")) + + err := s.addRemoteFwd(&fwa) + if err != nil { + return dbus.MakeFailedError(err) + } + return nil +} + +func (s *State) RemoveLocalForward(addr string) *dbus.Error { + var fwa FwdAddr + + if err := fwa.Set(addr); err != nil { + return dbus.MakeFailedError(err) + } + + fwa.SetDefaultAddr(netParseIP("127.0.0.1"), netParseIP("10.0.2.100")) + + if err := s.removeLocalFwd(&fwa); err != nil { + return dbus.MakeFailedError(err) + } + + return nil +} + +func (s *State) AddLocalForward(addr string) *dbus.Error { + var fwa FwdAddr + + if err := fwa.Set(addr); err != nil { + return dbus.MakeFailedError(err) + } + + fwa.SetDefaultAddr(netParseIP("127.0.0.1"), netParseIP("10.0.2.100")) + + err := s.addLocalFwd(&fwa) + if err != nil { + return dbus.MakeFailedError(err) + } + return nil +} + +func getForwardList(m map[string]*FwdAddr) []forwardEntry { + e := []forwardEntry{} + for _, b := range m { + e = append(e, forwardEntry{ + b.network, + b.bind.Addr.String(), + b.bind.Port, + b.host.Addr.String(), + b.host.Port, + }) + } + return e +} + +func (s *State) ListRemoteForward() ([]forwardEntry, *dbus.Error) { + e := getForwardList(s.remoteTcpFwd) + e = append(e, getForwardList(s.remoteUdpFwd)...) + return e, nil +} + +func (s *State) ListLocalForward() ([]forwardEntry, *dbus.Error) { + e := getForwardList(s.localTcpFwd) + e = append(e, getForwardList(s.localUdpFwd)...) + return e, nil +} + +func (s *State) Quit() *dbus.Error { + s.quitCh <- true + return nil +} + +func connect(addr string) (*dbus.Conn, error) { + conn, err := dbus.Dial(addr) + if err != nil { + return nil, err + } + if err = conn.Auth(nil); err != nil { + return nil, err + } + if err = conn.Hello(); err != nil { + return nil, err + } + + return conn, nil +} + +func setupDBus(state *State, addr string) error { + if addr == "" { + return nil + } + + conn, err := connect(addr) + if err != nil { + return err + } + + conn.Export(state, "/org/freedesktop/SlirpHelper1", "org.freedesktop.SlirpHelper1") + n := &introspect.Node{ + Name: "/org/freedesktop/SlirpHelper1", + Interfaces: []introspect.Interface{ + introspect.IntrospectData, + { + Name: "org.freedesktop.SlirpHelper1", + Methods: introspect.Methods(state), + }, + }, + } + conn.Export(introspect.NewIntrospectable(n), "/org/freedesktop/SlirpHelper1", + "org.freedesktop.DBus.Introspectable") + + if reply, err := conn.RequestName( + fmt.Sprintf("org.freedesktop.Slirp1_%d", os.Getpid()), + dbus.NameFlagDoNotQueue); err != nil { + return err + } else if reply != dbus.RequestNameReplyPrimaryOwner { + return errors.New("DBus name already taken") + } + + state.dbus = conn + return nil +} diff --git a/fwd.go b/fwd.go index 2206296..8151ff5 100644 --- a/fwd.go +++ b/fwd.go @@ -12,7 +12,7 @@ type Listener interface { Addr() net.Addr } -func LocalForwardTCP(state *State, s *stack.Stack, rf *FwdAddr, doneChannel <-chan bool) (Listener, error) { +func LocalForwardTCP(state *State, s *stack.Stack, rf *FwdAddr) (Listener, error) { tmpBind := &net.TCPAddr{ IP: net.IP(rf.bind.Addr), Port: int(rf.bind.Port), @@ -28,14 +28,16 @@ func LocalForwardTCP(state *State, s *stack.Stack, rf *FwdAddr, doneChannel <-ch return nil, err } - go func() error { + go func() { for { nRemote, err := srv.Accept() if err != nil { - // Not sure when Accept() can error, - // nor what the correct resolution - // is. Most likely socket is closed. - return err + // Most likely socket is closed. + // Not sure when Accept() can error otherwise, + if logConnections { + fmt.Printf("[!] TCP local-fwd error: %s\n", err) + } + return } remote := &KaTCPConn{nRemote.(*net.TCPConn)} @@ -45,6 +47,10 @@ func LocalForwardTCP(state *State, s *stack.Stack, rf *FwdAddr, doneChannel <-ch } }() + sa := srv.Addr().(*net.TCPAddr) + rf.bind.Port = uint16(sa.Port) + rf.listener = srv + state.localTcpFwd[rf.HostAddr().String()] = rf return srv, nil } @@ -56,7 +62,7 @@ func (u *UDPListner) Addr() net.Addr { return u.UDPConn.LocalAddr() } -func LocalForwardUDP(state *State, s *stack.Stack, rf *FwdAddr, doneChannel <-chan bool) (Listener, error) { +func LocalForwardUDP(state *State, s *stack.Stack, rf *FwdAddr) (Listener, error) { tmpBind := &net.UDPAddr{ IP: net.IP(rf.bind.Addr), Port: int(rf.bind.Port), @@ -102,7 +108,11 @@ func LocalForwardUDP(state *State, s *stack.Stack, rf *FwdAddr, doneChannel <-ch }() } }() - return &UDPListner{srv}, nil + + rf.listener = &UDPListner{srv} + state.localUdpFwd[rf.HostAddr().String()] = rf + + return rf.listener, nil } func LocalForward(state *State, s *stack.Stack, local KaConn, gaddr net.Addr, buf []byte, proxyProtocol bool) { diff --git a/go.mod b/go.mod index a36c02f..4a12e0f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/cenkalti/backoff v2.2.1+incompatible // indirect + github.com/godbus/dbus/v5 v5.0.3 github.com/golang/protobuf v1.3.3 // indirect github.com/opencontainers/runc v0.1.1 github.com/opencontainers/runtime-spec v0.1.2-0.20171211145439-b2d941ef6a78 diff --git a/go.sum b/go.sum index ff3073a..354affd 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/cenkalti/backoff v0.0.0-20190506075156-2146c9339422 h1:+FKjzBIdfBHYDv github.com/cenkalti/backoff v0.0.0-20190506075156-2146c9339422/go.mod h1:b6Nc7NRH5C4aCISLry0tLnTjcuTEvoiqcWDdsU0sOGM= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.6.1-0.20180915234121-886344bea079 h1:JFTFz3HZTGmgMz4E1TabNBNJljROSYgja1b4l50FNVs= github.com/gofrs/flock v0.6.1-0.20180915234121-886344bea079/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -20,6 +22,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/subcommands v0.0.0-20190508160503-636abe8753b8/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v0.0.0-20171129191014-dec09d789f3d/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runtime-spec v0.1.2-0.20171211145439-b2d941ef6a78 h1:d9F+LNYwMyi3BDN4GzZdaSiq4otb8duVEWyZjeUtOQI= github.com/opencontainers/runtime-spec v0.1.2-0.20171211145439-b2d941ef6a78/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8= diff --git a/main.go b/main.go index f004020..53bae75 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "errors" "flag" "fmt" "math/rand" @@ -14,6 +15,7 @@ import ( "github.com/opencontainers/runc/libcontainer/system" "golang.org/x/sys/unix" + "github.com/godbus/dbus/v5" "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/tcpip/link/sniffer" "gvisor.dev/gvisor/pkg/tcpip/stack" @@ -34,6 +36,7 @@ var ( gomaxprocs int pcapPath string exitWithParent bool + dbusAddress string ) func init() { @@ -48,6 +51,7 @@ func init() { flag.IntVar(&gomaxprocs, "maxprocs", 0, "set GOMAXPROCS variable to limit cpu") flag.StringVar(&pcapPath, "pcap", "", "path to PCAP file") flag.BoolVar(&exitWithParent, "exit-with-parent", false, "Exit with parent process") + flag.StringVar(&dbusAddress, "dbus-address", "", "DBus bus to connect to") } func main() { @@ -56,11 +60,89 @@ func main() { } type State struct { + stack *stack.Stack RoutingDeny []*net.IPNet RoutingAllow []*net.IPNet remoteUdpFwd map[string]*FwdAddr remoteTcpFwd map[string]*FwdAddr + localUdpFwd map[string]*FwdAddr + localTcpFwd map[string]*FwdAddr + + dbus *dbus.Conn + quitCh chan bool +} + +func (s *State) addLocalFwd(lf *FwdAddr) error { + srv, err := func() (Listener, error) { + switch lf.network { + case "tcp": + return LocalForwardTCP(s, s.stack, lf) + case "udp": + return LocalForwardUDP(s, s.stack, lf) + } + return nil, errors.New("Unhandled protocol") + }() + if err != nil { + fmt.Fprintf(os.Stderr, "[!] Failed to listen on %s://%s:%d: %s\n", + lf.network, lf.bind.Addr, lf.bind.Port, err) + return err + } + + ppPrefix := "" + if lf.proxyProtocol { + ppPrefix = "PP " + } + laddr := srv.Addr() + fmt.Printf("[+] local-fwd Local %slisten %s://%s\n", + ppPrefix, laddr.Network(), laddr.String()) + return nil +} + +func (s *State) removeLocalFwd(lf *FwdAddr) error { + switch lf.network { + case "tcp": + f := s.localTcpFwd[lf.HostAddr().String()] + f.listener.Close() + delete(s.localTcpFwd, lf.HostAddr().String()) + return nil + case "udp": + f := s.localUdpFwd[lf.HostAddr().String()] + f.listener.Close() + delete(s.localUdpFwd, lf.HostAddr().String()) + return nil + } + + return errors.New("Unhandled protocol") +} + +func (s *State) addRemoteFwd(rf *FwdAddr) error { + fmt.Printf("[+] Accepting on remote side %s://%s:%d\n", + rf.network, rf.bind.Addr.String(), rf.bind.Port) + + switch rf.network { + case "tcp": + s.remoteTcpFwd[rf.BindAddr().String()] = rf + return nil + case "udp": + s.remoteUdpFwd[rf.BindAddr().String()] = rf + return nil + } + + return errors.New("Unhandled protocol") +} + +func (s *State) removeRemoteFwd(rf *FwdAddr) error { + switch rf.network { + case "tcp": + delete(s.remoteTcpFwd, rf.BindAddr().String()) + return nil + case "udp": + delete(s.remoteUdpFwd, rf.BindAddr().String()) + return nil + } + + return errors.New("Unhandled protocol") } func Main() int { @@ -100,8 +182,11 @@ func Main() int { netParseIP("10.0.2.2"), netParseIP("127.0.0.1")) + state.quitCh = make(chan bool) state.remoteUdpFwd = make(map[string]*FwdAddr) state.remoteTcpFwd = make(map[string]*FwdAddr) + state.localUdpFwd = make(map[string]*FwdAddr) + state.localTcpFwd = make(map[string]*FwdAddr) // For the list of reserved IP's see // https://idea.popcount.org/2019-12-06-addressing/ The idea // here is to forbid outbound connections to obviously wrong @@ -154,6 +239,7 @@ func Main() int { bufSize := 4 * 1024 * 1024 s := NewStack(bufSize, bufSize) + state.stack = s if linkEP, err = createLinkEP(s, fd, tapMode, mac, uint32(mtu)); err != nil { panic(fmt.Sprintf("Failed to create linkEP: %s", err)) @@ -180,41 +266,17 @@ func Main() int { StackRoutingSetup(s, 1, "2001:2::2/32") - doneChannel := make(chan bool) + if err = setupDBus(&state, dbusAddress); err != nil { + fmt.Fprintf(os.Stderr, "[!] Failed to setup DBus: %s\n", err) + return 1 + } - for _, lf := range localFwd { - var srv Listener - switch lf.network { - case "tcp": - srv, err = LocalForwardTCP(&state, s, &lf, doneChannel) - case "udp": - srv, err = LocalForwardUDP(&state, s, &lf, doneChannel) - } - if err != nil { - fmt.Fprintf(os.Stderr, "[!] Failed to listen on %s://%s:%d: %s\n", - lf.network, lf.bind.Addr, lf.bind.Port, err) - } else { - ppPrefix := "" - if lf.proxyProtocol { - ppPrefix = "PP " - } - laddr := srv.Addr() - fmt.Printf("[+] local-fwd Local %slisten %s://%s\n", - ppPrefix, - laddr.Network(), - laddr.String()) - } + for i, _ := range localFwd { + state.addLocalFwd(&localFwd[i]) } - for i, rf := range remoteFwd { - fmt.Printf("[+] Accepting on remote side %s://%s:%d\n", - rf.network, rf.bind.Addr.String(), rf.bind.Port) - switch rf.network { - case "tcp": - state.remoteTcpFwd[rf.BindAddr().String()] = &remoteFwd[i] - case "udp": - state.remoteUdpFwd[rf.BindAddr().String()] = &remoteFwd[i] - } + for i, _ := range remoteFwd { + state.addRemoteFwd(&remoteFwd[i]) } tcpHandler := TcpRoutingHandler(&state) @@ -234,13 +296,15 @@ func Main() int { for { select { + case <-state.quitCh: + goto stop case sig := <-sigCh: signal.Reset(sig) - fmt.Fprintf(os.Stderr, "[-] Closing\n") goto stop } } stop: + fmt.Fprintf(os.Stderr, "[-] Closing\n") // TODO: define semantics of graceful close on signal //s.Wait() if metrics != nil { diff --git a/tests/base.py b/tests/base.py index c79d4fe..52551b9 100644 --- a/tests/base.py +++ b/tests/base.py @@ -21,6 +21,7 @@ MOCKHTTPSERVER = os.environ.get('MOCKHTTPSERVER', './tests/mockhttpserver/mockhttpserver') MOCKUDPECHO = os.environ.get('MOCKUDPECHO', './bin/mockudpecho') MOCKTCPECHO = os.environ.get('MOCKTCPECHO', './bin/mocktcpecho') +DBUS_SESSION_BUS_ADDRESS = os.environ.get('DBUS_SESSION_BUS_ADDRESS') IP_FREEBIND = 15 execno = 0 diff --git a/tests/dbus.conf b/tests/dbus.conf new file mode 100644 index 0000000..8e3efd2 --- /dev/null +++ b/tests/dbus.conf @@ -0,0 +1,22 @@ + + + + +slirpnetstack-test + +unix:dir=/tmp +EXTERNAL + + + + + + + + + + +contexts/dbus_contexts + + diff --git a/tests/test_basic.py b/tests/test_basic.py index 6b1f42c..d103068 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -5,6 +5,7 @@ import struct import unittest import urllib.request +from pydbus import SessionBus class BasicTest(base.TestCase): @@ -126,6 +127,18 @@ def test_metric(self): f = urllib.request.urlopen('http://127.0.0.1:%d/debug/pprof' % (metrics_port,)) self.assertIn(b"Types of profiles available:", f.read(300)) + def test_dbus(self): + ''' Test if -dbus-address works. ''' + if not base.DBUS_SESSION_BUS_ADDRESS: + self.skipTest("DBUS_SESSION_BUS_ADDRESS unset") + p = self.prun("-dbus-address %s" % base.DBUS_SESSION_BUS_ADDRESS) + self.assertStartSync(p) + bus = SessionBus() + iface = bus.get(".Slirp1_%u" % p.p.pid, "/org/freedesktop/SlirpHelper1") + info = iface.GetInfo() + self.assertIn("Protocol[State]", info) + iface.Quit() + class RoutingTest(base.TestCase): @base.isolateHostNetwork() @@ -428,6 +441,34 @@ def test_udp_local_fwd_merge(self): self.assertEqual(b"kota", s.recv(1024)) s.close() + def test_fwd_dbus(self): + ''' Test forwarding dbus API ''' + if not base.DBUS_SESSION_BUS_ADDRESS: + self.skipTest("DBUS_SESSION_BUS_ADDRESS unset") + + g_echo_port = self.start_tcp_echo(guest=True) + p = self.prun("-dbus-address %s" % base.DBUS_SESSION_BUS_ADDRESS) + self.assertStartSync(p) + bus = SessionBus() + iface = bus.get(".Slirp1_%u" % p.p.pid, "/org/freedesktop/SlirpHelper1") + + l = iface.ListLocalForward() + self.assertEqual(l, []) + + iface.AddLocalForward("0:10.0.2.100:%s" % g_echo_port) + port = self.assertListenLine(p, "local-fwd Local listen") + with self.guest_netns(): + self.assertTcpEcho(ip="127.0.0.1", port=g_echo_port) + self.assertTcpEcho(ip="127.0.0.1", port=port) + self.assertTcpEcho(ip="127.0.0.1", port=port) + self.assertIn("local-fwd conn", p.stdout_line()) + l = iface.ListLocalForward() + self.assertEqual(l, [('tcp', '127.0.0.1', port, '10.0.2.100', g_echo_port)]) + + iface.RemoveLocalForward("0:10.0.2.100:%s" % g_echo_port) + l = iface.ListLocalForward() + self.assertEqual(l, []) + class LocalForwardingPPTest(base.TestCase): def test_tcp_pp_local_fwd(self):