mirror of
https://github.com/go-micro/go-micro.git
synced 2024-11-24 08:02:32 +02:00
7b385bf163
* logs alloc Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * fix allocs Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * fix allocs Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * tunnel allocs Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * try to fix tunnel Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * cache cipher for send Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * more logger Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * more logger Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * more logger Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * more logger Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * more logger Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * more logger Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org> * more logger Signed-off-by: Vasiliy Tolstov <v.tolstov@unistack.org>
1421 lines
32 KiB
Go
1421 lines
32 KiB
Go
package tunnel
|
|
|
|
import (
|
|
"errors"
|
|
"math/rand"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/micro/go-micro/v2/logger"
|
|
"github.com/micro/go-micro/v2/transport"
|
|
)
|
|
|
|
var (
|
|
// DiscoverTime sets the time at which we fire discover messages
|
|
DiscoverTime = 30 * time.Second
|
|
// KeepAliveTime defines time interval we send keepalive messages to outbound links
|
|
KeepAliveTime = 30 * time.Second
|
|
// ReconnectTime defines time interval we periodically attempt to reconnect dead links
|
|
ReconnectTime = 5 * time.Second
|
|
)
|
|
|
|
// tun represents a network tunnel
|
|
type tun struct {
|
|
options Options
|
|
|
|
sync.RWMutex
|
|
|
|
// the unique id for this tunnel
|
|
id string
|
|
|
|
// tunnel token for session encryption
|
|
token string
|
|
|
|
// to indicate if we're connected or not
|
|
connected bool
|
|
|
|
// the send channel for all messages
|
|
send chan *message
|
|
|
|
// close channel
|
|
closed chan bool
|
|
|
|
// a map of sessions based on Micro-Tunnel-Channel
|
|
sessions map[string]*session
|
|
|
|
// outbound links
|
|
links map[string]*link
|
|
|
|
// listener
|
|
listener transport.Listener
|
|
}
|
|
|
|
// create new tunnel on top of a link
|
|
func newTunnel(opts ...Option) *tun {
|
|
rand.Seed(time.Now().UnixNano())
|
|
options := DefaultOptions()
|
|
for _, o := range opts {
|
|
o(&options)
|
|
}
|
|
|
|
return &tun{
|
|
options: options,
|
|
id: options.Id,
|
|
token: options.Token,
|
|
send: make(chan *message, 128),
|
|
closed: make(chan bool),
|
|
sessions: make(map[string]*session),
|
|
links: make(map[string]*link),
|
|
}
|
|
}
|
|
|
|
// Init initializes tunnel options
|
|
func (t *tun) Init(opts ...Option) error {
|
|
t.Lock()
|
|
for _, o := range opts {
|
|
o(&t.options)
|
|
}
|
|
t.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// getSession returns a session from the internal session map.
|
|
// It does this based on the Micro-Tunnel-Channel and Micro-Tunnel-Session
|
|
func (t *tun) getSession(channel, session string) (*session, bool) {
|
|
// get the session
|
|
t.RLock()
|
|
s, ok := t.sessions[channel+session]
|
|
t.RUnlock()
|
|
return s, ok
|
|
}
|
|
|
|
// delSession deletes a session if it exists
|
|
func (t *tun) delSession(channel, session string) {
|
|
t.Lock()
|
|
if s, ok := t.sessions[channel+session]; ok {
|
|
s.Close()
|
|
}
|
|
delete(t.sessions, channel+session)
|
|
t.Unlock()
|
|
}
|
|
|
|
// listChannels returns a list of listening channels
|
|
func (t *tun) listChannels() []string {
|
|
t.RLock()
|
|
|
|
//nolint:prealloc
|
|
var channels []string
|
|
for _, session := range t.sessions {
|
|
if session.session != "listener" {
|
|
continue
|
|
}
|
|
channels = append(channels, session.channel)
|
|
}
|
|
|
|
t.RUnlock()
|
|
|
|
return channels
|
|
}
|
|
|
|
// newSession creates a new session and saves it
|
|
func (t *tun) newSession(channel, sessionId string) (*session, bool, error) {
|
|
|
|
// new session
|
|
s := &session{
|
|
tunnel: t.id,
|
|
channel: channel,
|
|
session: sessionId,
|
|
token: t.token,
|
|
closed: make(chan bool),
|
|
recv: make(chan *message, 128),
|
|
send: t.send,
|
|
errChan: make(chan error, 1),
|
|
key: []byte(t.token + channel + sessionId),
|
|
}
|
|
gcm, err := newCipher(s.key)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
s.gcm = gcm
|
|
|
|
// save session
|
|
t.Lock()
|
|
_, ok := t.sessions[channel+sessionId]
|
|
if ok {
|
|
// session already exists
|
|
t.Unlock()
|
|
return nil, false, nil
|
|
}
|
|
|
|
t.sessions[channel+sessionId] = s
|
|
t.Unlock()
|
|
|
|
// return session
|
|
return s, true, nil
|
|
}
|
|
|
|
// TODO: use tunnel id as part of the session
|
|
func (t *tun) newSessionId() string {
|
|
return uuid.New().String()
|
|
}
|
|
|
|
// announce will send a message to the link to tell the other side of a channel mapping we have.
|
|
// This usually happens if someone calls Dial and sends a discover message but otherwise we
|
|
// periodically send these messages to asynchronously manage channel mappings.
|
|
func (t *tun) announce(channel, session string, link *link) {
|
|
// create the "announce" response message for a discover request
|
|
msg := &transport.Message{
|
|
Header: map[string]string{
|
|
"Micro-Tunnel": "announce",
|
|
"Micro-Tunnel-Id": t.id,
|
|
"Micro-Tunnel-Channel": channel,
|
|
"Micro-Tunnel-Session": session,
|
|
"Micro-Tunnel-Link": link.id,
|
|
},
|
|
}
|
|
|
|
// if no channel is present we've been asked to discover all channels
|
|
if len(channel) == 0 {
|
|
// get the list of channels
|
|
channels := t.listChannels()
|
|
|
|
// if there are no channels continue
|
|
if len(channels) == 0 {
|
|
return
|
|
}
|
|
|
|
// create a list of channels as comma separated list
|
|
channel = strings.Join(channels, ",")
|
|
// set channels as header
|
|
msg.Header["Micro-Tunnel-Channel"] = channel
|
|
} else {
|
|
// otherwise look for a single channel mapping
|
|
// looking for existing mapping as a listener
|
|
_, exists := t.getSession(channel, "listener")
|
|
if !exists {
|
|
return
|
|
}
|
|
}
|
|
|
|
if logger.V(logger.TraceLevel, log) {
|
|
log.Debugf("Tunnel sending announce for discovery of channel(s) %s", channel)
|
|
}
|
|
// send back the announcement
|
|
if err := link.Send(msg); err != nil {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel failed to send announcement for channel(s) %s message: %v", channel, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// manage monitors outbound links and attempts to reconnect to the failed ones
|
|
func (t *tun) manage(reconnect time.Duration) {
|
|
r := time.NewTicker(reconnect)
|
|
defer r.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-t.closed:
|
|
return
|
|
case <-r.C:
|
|
t.manageLinks()
|
|
}
|
|
}
|
|
}
|
|
|
|
// manageLink sends channel discover requests periodically and
|
|
// keepalive messages to link
|
|
func (t *tun) manageLink(link *link) {
|
|
keepalive := time.NewTicker(KeepAliveTime)
|
|
defer keepalive.Stop()
|
|
discover := time.NewTicker(DiscoverTime)
|
|
defer discover.Stop()
|
|
|
|
wait := func(d time.Duration) {
|
|
// jitter
|
|
j := rand.Int63n(int64(d.Seconds() / 2.0))
|
|
time.Sleep(time.Duration(j) * time.Second)
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-t.closed:
|
|
return
|
|
case <-link.closed:
|
|
return
|
|
case <-discover.C:
|
|
// wait half the discover time
|
|
wait(DiscoverTime)
|
|
|
|
// send a discovery message to the link
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel sending discover to link: %v", link.Remote())
|
|
}
|
|
if err := t.sendMsg("discover", link); err != nil {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel failed to send discover to link %s: %v", link.Remote(), err)
|
|
}
|
|
}
|
|
case <-keepalive.C:
|
|
// wait half the keepalive time
|
|
wait(KeepAliveTime)
|
|
|
|
// send keepalive message
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel sending keepalive to link: %v", link.Remote())
|
|
}
|
|
if err := t.sendMsg("keepalive", link); err != nil {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel error sending keepalive to link %v: %v", link.Remote(), err)
|
|
}
|
|
t.delLink(link.Remote())
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// manageLinks is a function that can be called to immediately to link setup
|
|
// it purges dead links while generating new links for any nodes not connected
|
|
func (t *tun) manageLinks() {
|
|
delLinks := make(map[*link]string)
|
|
connected := make(map[string]bool)
|
|
|
|
t.RLock()
|
|
|
|
// get list of nodes from options
|
|
nodes := t.options.Nodes
|
|
|
|
// check the link status and purge dead links
|
|
for node, link := range t.links {
|
|
// check link status
|
|
switch link.State() {
|
|
case "closed", "error":
|
|
delLinks[link] = node
|
|
default:
|
|
connected[node] = true
|
|
}
|
|
}
|
|
|
|
t.RUnlock()
|
|
|
|
// build a list of links to connect to
|
|
var connect []string
|
|
|
|
for _, node := range nodes {
|
|
// check if we're connected
|
|
if _, ok := connected[node]; ok {
|
|
continue
|
|
}
|
|
// add nodes to connect o
|
|
connect = append(connect, node)
|
|
}
|
|
|
|
// delete the dead links
|
|
if len(delLinks) > 0 {
|
|
t.Lock()
|
|
|
|
for link, node := range delLinks {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel deleting dead link for %s", node)
|
|
}
|
|
// check if the link exists
|
|
l, ok := t.links[node]
|
|
if ok {
|
|
// close and delete
|
|
l.Close()
|
|
delete(t.links, node)
|
|
}
|
|
|
|
// if the link does not match our own
|
|
if l != link {
|
|
// close our link just in case
|
|
link.Close()
|
|
}
|
|
}
|
|
|
|
t.Unlock()
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// establish new links
|
|
|
|
for _, node := range connect {
|
|
wg.Add(1)
|
|
|
|
go func(node string) {
|
|
defer wg.Done()
|
|
|
|
// create new link
|
|
// if we're using quic it should be a max 10 second handshake period
|
|
link, err := t.setupLink(node)
|
|
if err != nil {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel failed to setup node link to %s: %v", node, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
t.Lock()
|
|
|
|
// just check nothing else was setup in the interim
|
|
if _, ok := t.links[node]; ok {
|
|
link.Close()
|
|
t.Unlock()
|
|
return
|
|
}
|
|
|
|
// save the link
|
|
t.links[node] = link
|
|
|
|
t.Unlock()
|
|
}(node)
|
|
}
|
|
|
|
// wait for all threads to finish
|
|
wg.Wait()
|
|
}
|
|
|
|
// process outgoing messages sent by all local sessions
|
|
func (t *tun) process() {
|
|
// manage the send buffer
|
|
// all pseudo sessions throw everything down this
|
|
for {
|
|
select {
|
|
case msg := <-t.send:
|
|
// build a list of links to send to
|
|
var sendTo []*link
|
|
var err error
|
|
|
|
t.RLock()
|
|
|
|
// build the list of links ot send to
|
|
for _, link := range t.links {
|
|
// get the values we need
|
|
link.RLock()
|
|
id := link.id
|
|
connected := link.connected
|
|
loopback := link.loopback
|
|
_, exists := link.channels[msg.channel]
|
|
link.RUnlock()
|
|
|
|
// if the link is not connected skip it
|
|
if !connected {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Link for node %s not connected", id)
|
|
}
|
|
err = ErrLinkDisconnected
|
|
continue
|
|
}
|
|
|
|
// if the link was a loopback accepted connection
|
|
// and the message is being sent outbound via
|
|
// a dialled connection don't use this link
|
|
if loopback && msg.outbound {
|
|
err = ErrLinkLoopback
|
|
continue
|
|
}
|
|
|
|
// if the message was being returned by the loopback listener
|
|
// send it back up the loopback link only
|
|
if msg.loopback && !loopback {
|
|
err = ErrLinkRemote
|
|
continue
|
|
}
|
|
|
|
// check the multicast mappings
|
|
if msg.mode == Multicast {
|
|
// channel mapping not found in link
|
|
if !exists {
|
|
continue
|
|
}
|
|
} else {
|
|
// if we're picking the link check the id
|
|
// this is where we explicitly set the link
|
|
// in a message received via the listen method
|
|
if len(msg.link) > 0 && id != msg.link {
|
|
err = ErrLinkNotFound
|
|
continue
|
|
}
|
|
}
|
|
|
|
// add to link list
|
|
sendTo = append(sendTo, link)
|
|
}
|
|
|
|
t.RUnlock()
|
|
|
|
// no links to send to
|
|
if len(sendTo) == 0 {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("No links to send message type: %s channel: %s", msg.typ, msg.channel)
|
|
}
|
|
t.respond(msg, err)
|
|
continue
|
|
}
|
|
|
|
// send the message
|
|
go t.sendTo(sendTo, msg)
|
|
case <-t.closed:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// send response back for a message to the caller
|
|
func (t *tun) respond(msg *message, err error) {
|
|
select {
|
|
case msg.errChan <- err:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// sendTo sends a message to the chosen links
|
|
func (t *tun) sendTo(links []*link, msg *message) error {
|
|
// the function that sends the actual message
|
|
send := func(link *link, msg *transport.Message) error {
|
|
if err := link.Send(msg); err != nil {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel error sending %+v to %s: %v", msg.Header, link.Remote(), err)
|
|
}
|
|
t.delLink(link.Remote())
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
newMsg := &transport.Message{
|
|
Header: make(map[string]string),
|
|
}
|
|
|
|
// set the data
|
|
if msg.data != nil {
|
|
for k, v := range msg.data.Header {
|
|
newMsg.Header[k] = v
|
|
}
|
|
newMsg.Body = msg.data.Body
|
|
}
|
|
|
|
// set message head
|
|
newMsg.Header["Micro-Tunnel"] = msg.typ
|
|
// set the tunnel id on the outgoing message
|
|
newMsg.Header["Micro-Tunnel-Id"] = msg.tunnel
|
|
// set the tunnel channel on the outgoing message
|
|
newMsg.Header["Micro-Tunnel-Channel"] = msg.channel
|
|
// set the session id
|
|
newMsg.Header["Micro-Tunnel-Session"] = msg.session
|
|
|
|
// error channel for call
|
|
errChan := make(chan error, len(links))
|
|
|
|
// execute in parallel
|
|
sendTo := func(l *link, m *transport.Message, errChan chan error) {
|
|
errChan <- send(l, m)
|
|
}
|
|
|
|
// send the message
|
|
for _, link := range links {
|
|
// send the message via the current link
|
|
if logger.V(logger.TraceLevel, log) {
|
|
log.Tracef("Tunnel sending %+v to %s", newMsg.Header, link.Remote())
|
|
}
|
|
|
|
// blast it in a go routine since its multicast/broadcast
|
|
if msg.mode > Unicast {
|
|
// make a copy
|
|
m := &transport.Message{
|
|
Header: make(map[string]string),
|
|
Body: make([]byte, len(newMsg.Body)),
|
|
}
|
|
copy(m.Body, newMsg.Body)
|
|
for k, v := range newMsg.Header {
|
|
m.Header[k] = v
|
|
}
|
|
|
|
go sendTo(link, m, errChan)
|
|
|
|
continue
|
|
}
|
|
|
|
// otherwise send as unicast
|
|
if err := send(link, newMsg); err != nil {
|
|
// put in the error chan if it failed
|
|
errChan <- err
|
|
continue
|
|
}
|
|
|
|
// sent successfully so just return
|
|
t.respond(msg, nil)
|
|
return nil
|
|
}
|
|
|
|
// either all unicast attempts failed or we're
|
|
// checking the multicast/broadcast attempts
|
|
|
|
var err error
|
|
|
|
// check all the errors
|
|
for i := 0; i < len(links); i++ {
|
|
err = <-errChan
|
|
// success
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
// return error. it's non blocking
|
|
t.respond(msg, err)
|
|
return err
|
|
}
|
|
|
|
func (t *tun) delLink(remote string) {
|
|
t.Lock()
|
|
|
|
// get the link
|
|
for id, link := range t.links {
|
|
if link.id != remote {
|
|
continue
|
|
}
|
|
// close and delete
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel deleting link node: %s remote: %s", id, link.Remote())
|
|
}
|
|
link.Close()
|
|
delete(t.links, id)
|
|
}
|
|
|
|
t.Unlock()
|
|
}
|
|
|
|
// process incoming messages
|
|
func (t *tun) listen(link *link) {
|
|
// remove the link on exit
|
|
defer func() {
|
|
t.delLink(link.Remote())
|
|
}()
|
|
|
|
// let us know if its a loopback
|
|
var loopback bool
|
|
var connected bool
|
|
|
|
// set the connected value
|
|
link.RLock()
|
|
connected = link.connected
|
|
link.RUnlock()
|
|
|
|
for {
|
|
// process anything via the net interface
|
|
msg := new(transport.Message)
|
|
if err := link.Recv(msg); err != nil {
|
|
log.Debugf("Tunnel link %s receive error: %v", link.Remote(), err)
|
|
return
|
|
}
|
|
|
|
// TODO: figure out network authentication
|
|
// for now we use tunnel token to encrypt/decrypt
|
|
// session communication, but we will probably need
|
|
// some sort of network authentication (token) to avoid
|
|
// having rogue actors spamming the network
|
|
|
|
// message type
|
|
mtype := msg.Header["Micro-Tunnel"]
|
|
// the tunnel id
|
|
id := msg.Header["Micro-Tunnel-Id"]
|
|
// the tunnel channel
|
|
channel := msg.Header["Micro-Tunnel-Channel"]
|
|
// the session id
|
|
sessionId := msg.Header["Micro-Tunnel-Session"]
|
|
|
|
// if its not connected throw away the link
|
|
// the first message we process needs to be connect
|
|
if !connected && mtype != "connect" {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel link %s not connected", link.id)
|
|
}
|
|
return
|
|
}
|
|
|
|
// this state machine block handles the only message types
|
|
// that we know or care about; connect, close, open, accept,
|
|
// discover, announce, session, keepalive
|
|
switch mtype {
|
|
case "connect":
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel link %s received connect message", link.Remote())
|
|
}
|
|
|
|
link.Lock()
|
|
|
|
// check if we're connecting to ourselves?
|
|
if id == t.id {
|
|
link.loopback = true
|
|
loopback = true
|
|
}
|
|
|
|
// set to remote node
|
|
link.id = link.Remote()
|
|
// set as connected
|
|
link.connected = true
|
|
connected = true
|
|
|
|
link.Unlock()
|
|
|
|
// save the link once connected
|
|
t.Lock()
|
|
t.links[link.Remote()] = link
|
|
t.Unlock()
|
|
|
|
// send back an announcement of our channels discovery
|
|
go t.announce("", "", link)
|
|
// ask for the things on the other wise
|
|
go t.sendMsg("discover", link)
|
|
// nothing more to do
|
|
continue
|
|
case "close":
|
|
// if there is no channel then we close the link
|
|
// as its a signal from the other side to close the connection
|
|
if len(channel) == 0 {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel link %s received close message", link.Remote())
|
|
}
|
|
return
|
|
}
|
|
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel link %s received close message for %s", link.Remote(), channel)
|
|
}
|
|
// the entire listener was closed by the remote side so we need to
|
|
// remove the channel mapping for it. should we also close sessions?
|
|
if sessionId == "listener" {
|
|
link.delChannel(channel)
|
|
// TODO: find all the non listener unicast sessions
|
|
// and close them. think aboud edge cases first
|
|
continue
|
|
}
|
|
|
|
// assuming there's a channel and session
|
|
// try get the dialing socket
|
|
s, exists := t.getSession(channel, sessionId)
|
|
if exists && !loopback {
|
|
// only delete the session if its unicast
|
|
// otherwise ignore close on the multicast
|
|
if s.mode == Unicast {
|
|
// only delete this if its unicast
|
|
// but not if its a loopback conn
|
|
t.delSession(channel, sessionId)
|
|
continue
|
|
}
|
|
}
|
|
// otherwise its a session mapping of sorts
|
|
case "keepalive":
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel link %s received keepalive", link.Remote())
|
|
}
|
|
// save the keepalive
|
|
link.keepalive()
|
|
continue
|
|
// a new connection dialled outbound
|
|
case "open":
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel link %s received open %s %s", link.id, channel, sessionId)
|
|
}
|
|
// we just let it pass through to be processed
|
|
// an accept returned by the listener
|
|
case "accept":
|
|
s, exists := t.getSession(channel, sessionId)
|
|
// just set accepted on anything not unicast
|
|
if exists && s.mode > Unicast {
|
|
s.accepted = true
|
|
continue
|
|
}
|
|
// if its already accepted move on
|
|
if exists && s.accepted {
|
|
continue
|
|
}
|
|
// otherwise we're going to process to accept
|
|
// a continued session
|
|
case "session":
|
|
// process message
|
|
if logger.V(logger.TraceLevel, log) {
|
|
log.Tracef("Tunnel received %+v from %s", msg.Header, link.Remote())
|
|
}
|
|
// an announcement of a channel listener
|
|
case "announce":
|
|
if logger.V(logger.TraceLevel, log) {
|
|
log.Tracef("Tunnel received %+v from %s", msg.Header, link.Remote())
|
|
}
|
|
// process the announcement
|
|
channels := strings.Split(channel, ",")
|
|
|
|
// update mapping in the link
|
|
link.setChannel(channels...)
|
|
|
|
// this was an announcement not intended for anything
|
|
// if the dialing side sent "discover" then a session
|
|
// id would be present. We skip in case of multicast.
|
|
switch sessionId {
|
|
case "listener", "multicast", "":
|
|
continue
|
|
}
|
|
|
|
// get the session that asked for the discovery
|
|
s, exists := t.getSession(channel, sessionId)
|
|
if exists {
|
|
// don't bother it's already discovered
|
|
if s.discovered {
|
|
continue
|
|
}
|
|
|
|
msg := &message{
|
|
typ: "announce",
|
|
tunnel: id,
|
|
channel: channel,
|
|
session: sessionId,
|
|
link: link.id,
|
|
}
|
|
|
|
// send the announce back to the caller
|
|
select {
|
|
case <-s.closed:
|
|
case s.recv <- msg:
|
|
}
|
|
}
|
|
continue
|
|
case "discover":
|
|
// send back an announcement
|
|
go t.announce(channel, sessionId, link)
|
|
continue
|
|
default:
|
|
// blackhole it
|
|
continue
|
|
}
|
|
|
|
// strip tunnel message header
|
|
for k := range msg.Header {
|
|
if strings.HasPrefix(k, "Micro-Tunnel") {
|
|
delete(msg.Header, k)
|
|
}
|
|
}
|
|
|
|
// if the session id is blank there's nothing we can do
|
|
// TODO: check this is the case, is there any reason
|
|
// why we'd have a blank session? Is the tunnel
|
|
// used for some other purpose?
|
|
if len(channel) == 0 || len(sessionId) == 0 {
|
|
continue
|
|
}
|
|
|
|
var s *session
|
|
var exists bool
|
|
|
|
// If its a loopback connection then we've enabled link direction
|
|
// listening side is used for listening, the dialling side for dialling
|
|
switch {
|
|
case loopback, mtype == "open":
|
|
s, exists = t.getSession(channel, "listener")
|
|
// only return accept to the session
|
|
case mtype == "accept":
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel received accept message for channel: %s session: %s", channel, sessionId)
|
|
}
|
|
s, exists = t.getSession(channel, sessionId)
|
|
if exists && s.accepted {
|
|
continue
|
|
}
|
|
default:
|
|
// get the session based on the tunnel id and session
|
|
// this could be something we dialed in which case
|
|
// we have a session for it otherwise its a listener
|
|
s, exists = t.getSession(channel, sessionId)
|
|
if !exists {
|
|
// try get it based on just the tunnel id
|
|
// the assumption here is that a listener
|
|
// has no session but its set a listener session
|
|
s, exists = t.getSession(channel, "listener")
|
|
}
|
|
}
|
|
|
|
// bail if no session or listener has been found
|
|
if !exists {
|
|
if logger.V(logger.TraceLevel, log) {
|
|
log.Tracef("Tunnel skipping no channel: %s session: %s exists", channel, sessionId)
|
|
}
|
|
// drop it, we don't care about
|
|
// messages we don't know about
|
|
continue
|
|
}
|
|
|
|
// is the session closed?
|
|
select {
|
|
case <-s.closed:
|
|
// closed
|
|
delete(t.sessions, channel)
|
|
continue
|
|
default:
|
|
// otherwise process
|
|
}
|
|
if logger.V(logger.TraceLevel, log) {
|
|
log.Tracef("Tunnel using channel: %s session: %s type: %s", s.channel, s.session, mtype)
|
|
}
|
|
// construct a new transport message
|
|
tmsg := &transport.Message{
|
|
Header: msg.Header,
|
|
Body: msg.Body,
|
|
}
|
|
|
|
// construct the internal message
|
|
imsg := &message{
|
|
tunnel: id,
|
|
typ: mtype,
|
|
channel: channel,
|
|
session: sessionId,
|
|
mode: s.mode,
|
|
data: tmsg,
|
|
link: link.id,
|
|
loopback: loopback,
|
|
errChan: make(chan error, 1),
|
|
}
|
|
|
|
// append to recv backlog
|
|
// we don't block if we can't pass it on
|
|
select {
|
|
case s.recv <- imsg:
|
|
default:
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *tun) sendMsg(method string, link *link) error {
|
|
return link.Send(&transport.Message{
|
|
Header: map[string]string{
|
|
"Micro-Tunnel": method,
|
|
"Micro-Tunnel-Id": t.id,
|
|
},
|
|
})
|
|
}
|
|
|
|
// setupLink connects to node and returns link if successful
|
|
// It returns error if the link failed to be established
|
|
func (t *tun) setupLink(node string) (*link, error) {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel setting up link: %s", node)
|
|
}
|
|
c, err := t.options.Transport.Dial(node)
|
|
if err != nil {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel failed to connect to %s: %v", node, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel connected to %s", node)
|
|
}
|
|
// create a new link
|
|
link := newLink(c)
|
|
|
|
// set link id to remote side
|
|
link.Lock()
|
|
link.id = c.Remote()
|
|
link.Unlock()
|
|
|
|
// send the first connect message
|
|
if err := t.sendMsg("connect", link); err != nil {
|
|
link.Close()
|
|
return nil, err
|
|
}
|
|
|
|
// we made the outbound connection
|
|
// and sent the connect message
|
|
link.connected = true
|
|
|
|
// process incoming messages
|
|
go t.listen(link)
|
|
|
|
// manage keepalives and discovery messages
|
|
go t.manageLink(link)
|
|
|
|
return link, nil
|
|
}
|
|
|
|
func (t *tun) setupLinks() {
|
|
var wg sync.WaitGroup
|
|
|
|
for _, node := range t.options.Nodes {
|
|
wg.Add(1)
|
|
|
|
go func(node string) {
|
|
defer wg.Done()
|
|
|
|
// we're not trying to fix existing links
|
|
if _, ok := t.links[node]; ok {
|
|
return
|
|
}
|
|
|
|
// create new link
|
|
link, err := t.setupLink(node)
|
|
if err != nil {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel failed to setup node link to %s: %v", node, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
// save the link
|
|
t.links[node] = link
|
|
}(node)
|
|
}
|
|
|
|
// wait for all threads to finish
|
|
wg.Wait()
|
|
}
|
|
|
|
// connect the tunnel to all the nodes and listen for incoming tunnel connections
|
|
func (t *tun) connect() error {
|
|
l, err := t.options.Transport.Listen(t.options.Address)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// save the listener
|
|
t.listener = l
|
|
|
|
go func() {
|
|
// accept inbound connections
|
|
err := l.Accept(func(sock transport.Socket) {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel accepted connection from %s", sock.Remote())
|
|
}
|
|
// create a new link
|
|
link := newLink(sock)
|
|
|
|
// manage the link
|
|
go t.manageLink(link)
|
|
|
|
// listen for inbound messages.
|
|
// only save the link once connected.
|
|
// we do this inside liste
|
|
t.listen(link)
|
|
})
|
|
|
|
t.RLock()
|
|
defer t.RUnlock()
|
|
|
|
// still connected but the tunnel died
|
|
if err != nil && t.connected {
|
|
log.Errorf("Tunnel listener died: %v", err)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Connect the tunnel
|
|
func (t *tun) Connect() error {
|
|
t.Lock()
|
|
defer t.Unlock()
|
|
|
|
// already connected
|
|
if t.connected {
|
|
// do it immediately
|
|
t.setupLinks()
|
|
// setup links
|
|
return nil
|
|
}
|
|
|
|
// connect the tunnel: start the listener
|
|
if err := t.connect(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// set as connected
|
|
t.connected = true
|
|
// create new close channel
|
|
t.closed = make(chan bool)
|
|
|
|
// process outbound messages to be sent
|
|
// process sends to all links
|
|
go t.process()
|
|
|
|
// call setup before managing them
|
|
t.setupLinks()
|
|
|
|
// manage the links
|
|
go t.manage(ReconnectTime)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *tun) close() error {
|
|
// close all the sessions
|
|
for id, s := range t.sessions {
|
|
s.Close()
|
|
delete(t.sessions, id)
|
|
}
|
|
|
|
// close all the links
|
|
for node, link := range t.links {
|
|
link.Send(&transport.Message{
|
|
Header: map[string]string{
|
|
"Micro-Tunnel": "close",
|
|
"Micro-Tunnel-Id": t.id,
|
|
},
|
|
})
|
|
link.Close()
|
|
delete(t.links, node)
|
|
}
|
|
|
|
// close the listener
|
|
// this appears to be blocking
|
|
return t.listener.Close()
|
|
}
|
|
|
|
// pickLink will pick the best link based on connectivity, delay, rate and length
|
|
func (t *tun) pickLink(links []*link) *link {
|
|
var metric float64
|
|
var chosen *link
|
|
|
|
// find the best link
|
|
for i, link := range links {
|
|
// don't use disconnected or errored links
|
|
if link.State() != "connected" {
|
|
continue
|
|
}
|
|
|
|
// skip the loopback
|
|
if link.Loopback() {
|
|
continue
|
|
}
|
|
|
|
// get the link state info
|
|
d := float64(link.Delay())
|
|
l := float64(link.Length())
|
|
r := link.Rate()
|
|
|
|
// metric = delay x length x rate
|
|
m := d * l * r
|
|
|
|
// first link so just and go
|
|
if i == 0 {
|
|
metric = m
|
|
chosen = link
|
|
continue
|
|
}
|
|
|
|
// we found a better metric
|
|
if m < metric {
|
|
metric = m
|
|
chosen = link
|
|
}
|
|
}
|
|
|
|
// if there's no link we're just going to mess around
|
|
if chosen == nil {
|
|
i := rand.Intn(len(links))
|
|
return links[i]
|
|
}
|
|
|
|
// we chose the link with;
|
|
// the lowest delay e.g least messages queued
|
|
// the lowest rate e.g the least messages flowing
|
|
// the lowest length e.g the smallest roundtrip time
|
|
return chosen
|
|
}
|
|
|
|
func (t *tun) Address() string {
|
|
t.RLock()
|
|
defer t.RUnlock()
|
|
|
|
if !t.connected {
|
|
return t.options.Address
|
|
}
|
|
|
|
return t.listener.Addr()
|
|
}
|
|
|
|
// Close the tunnel
|
|
func (t *tun) Close() error {
|
|
t.Lock()
|
|
defer t.Unlock()
|
|
|
|
if !t.connected {
|
|
return nil
|
|
}
|
|
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debug("Tunnel closing")
|
|
}
|
|
|
|
select {
|
|
case <-t.closed:
|
|
return nil
|
|
default:
|
|
close(t.closed)
|
|
t.connected = false
|
|
}
|
|
|
|
// send a close message
|
|
// we don't close the link
|
|
// just the tunnel
|
|
return t.close()
|
|
}
|
|
|
|
// Dial an address
|
|
func (t *tun) Dial(channel string, opts ...DialOption) (Session, error) {
|
|
// get the options
|
|
options := DialOptions{
|
|
Timeout: DefaultDialTimeout,
|
|
Wait: true,
|
|
}
|
|
|
|
for _, o := range opts {
|
|
o(&options)
|
|
}
|
|
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel dialing %s", channel)
|
|
}
|
|
|
|
// create a new session
|
|
c, ok, err := t.newSession(channel, t.newSessionId())
|
|
if err != nil {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Error(err)
|
|
}
|
|
return nil, err
|
|
} else if !ok {
|
|
return nil, errors.New("error dialing " + channel)
|
|
}
|
|
|
|
// set remote
|
|
c.remote = channel
|
|
// set local
|
|
c.local = "local"
|
|
// outbound session
|
|
c.outbound = true
|
|
// set the mode of connection unicast/multicast/broadcast
|
|
c.mode = options.Mode
|
|
// set the dial timeout
|
|
c.dialTimeout = options.Timeout
|
|
// set read timeout set to never
|
|
c.readTimeout = time.Duration(-1)
|
|
// set the link
|
|
c.link = options.Link
|
|
|
|
var links []*link
|
|
// did we measure the rtt
|
|
var measured bool
|
|
|
|
t.RLock()
|
|
|
|
// non multicast so we need to find the link
|
|
for _, link := range t.links {
|
|
// use the link specified it its available
|
|
if len(c.link) > 0 && link.id != c.link {
|
|
continue
|
|
}
|
|
|
|
// get the channel
|
|
lastMapped := link.getChannel(channel)
|
|
|
|
// we have at least one channel mapping
|
|
if !lastMapped.IsZero() {
|
|
links = append(links, link)
|
|
c.discovered = true
|
|
}
|
|
}
|
|
|
|
t.RUnlock()
|
|
|
|
// link option was specified to pick the link
|
|
if len(options.Link) > 0 {
|
|
// link not found and one was specified so error out
|
|
if len(links) == 0 {
|
|
// delete session and return error
|
|
t.delSession(c.channel, c.session)
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel deleting session %s %s: %v", c.session, c.channel, ErrLinkNotFound)
|
|
}
|
|
return nil, ErrLinkNotFound
|
|
}
|
|
|
|
// assume discovered because we picked
|
|
c.discovered = true
|
|
|
|
// link asked for and found and now
|
|
// we've been asked not to wait so return
|
|
if !options.Wait {
|
|
c.accepted = true
|
|
return c, nil
|
|
}
|
|
}
|
|
|
|
// discovered so set the link if not multicast
|
|
if c.discovered && c.mode == Unicast {
|
|
// pick a link if not specified
|
|
if len(c.link) == 0 {
|
|
// pickLink will pick the best link
|
|
link := t.pickLink(links)
|
|
// set the link
|
|
c.link = link.id
|
|
}
|
|
}
|
|
|
|
// if its not already discovered we need to attempt to do so
|
|
if !c.discovered {
|
|
// piggy back roundtrip
|
|
nowRTT := time.Now()
|
|
|
|
// attempt to discover the link
|
|
err := c.Discover()
|
|
if err != nil {
|
|
t.delSession(c.channel, c.session)
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel deleting session %s %s: %v", c.session, c.channel, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// set roundtrip
|
|
d := time.Since(nowRTT)
|
|
|
|
// set the link time
|
|
t.RLock()
|
|
link, ok := t.links[c.link]
|
|
t.RUnlock()
|
|
|
|
if ok {
|
|
// set the rountrip time
|
|
link.setRTT(d)
|
|
// set measured to true
|
|
measured = true
|
|
}
|
|
}
|
|
|
|
// return early if its not unicast
|
|
// we will not wait for "open" for multicast
|
|
// and we will not wait it told not to
|
|
if c.mode != Unicast || !options.Wait {
|
|
return c, nil
|
|
}
|
|
|
|
// Note: we go no further for multicast or broadcast.
|
|
// This is a unicast session so we call "open" and wait
|
|
// for an "accept"
|
|
|
|
// reset now in case we use it
|
|
now := time.Now()
|
|
|
|
// try to open the session
|
|
if err := c.Open(); err != nil {
|
|
// delete the session
|
|
t.delSession(c.channel, c.session)
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel deleting session %s %s: %v", c.session, c.channel, err)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// set time take to open
|
|
d := time.Since(now)
|
|
|
|
// if we haven't measured the roundtrip do it now
|
|
if !measured {
|
|
// set the link time
|
|
t.RLock()
|
|
link, ok := t.links[c.link]
|
|
t.RUnlock()
|
|
|
|
if ok {
|
|
// set the rountrip time
|
|
link.setRTT(d)
|
|
}
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// Accept a connection on the address
|
|
func (t *tun) Listen(channel string, opts ...ListenOption) (Listener, error) {
|
|
if logger.V(logger.DebugLevel, log) {
|
|
log.Debugf("Tunnel listening on %s", channel)
|
|
}
|
|
options := ListenOptions{
|
|
// Read timeout defaults to never
|
|
Timeout: time.Duration(-1),
|
|
}
|
|
|
|
for _, o := range opts {
|
|
o(&options)
|
|
}
|
|
|
|
// create a new session by hashing the address
|
|
c, ok, err := t.newSession(channel, "listener")
|
|
if err != nil {
|
|
if logger.V(logger.ErrorLevel, log) {
|
|
log.Error(err)
|
|
}
|
|
return nil, err
|
|
} else if !ok {
|
|
return nil, errors.New("already listening on " + channel)
|
|
}
|
|
|
|
// delete function removes the session when closed
|
|
delFunc := func() {
|
|
t.delSession(channel, "listener")
|
|
}
|
|
|
|
// set remote. it will be replaced by the first message received
|
|
c.remote = "remote"
|
|
// set local
|
|
c.local = channel
|
|
// set mode
|
|
c.mode = options.Mode
|
|
// set the timeout
|
|
c.readTimeout = options.Timeout
|
|
|
|
tl := &tunListener{
|
|
channel: channel,
|
|
// tunnel token
|
|
token: t.token,
|
|
// the accept channel
|
|
accept: make(chan *session, 128),
|
|
// the channel to close
|
|
closed: make(chan bool),
|
|
// tunnel closed channel
|
|
tunClosed: t.closed,
|
|
// the listener session
|
|
session: c,
|
|
// delete session
|
|
delFunc: delFunc,
|
|
}
|
|
|
|
// this kicks off the internal message processor
|
|
// for the listener so it can create pseudo sessions
|
|
// per session if they do not exist or pass messages
|
|
// to the existign sessions
|
|
go tl.process()
|
|
|
|
// return the listener
|
|
return tl, nil
|
|
}
|
|
|
|
func (t *tun) Links() []Link {
|
|
t.RLock()
|
|
links := make([]Link, 0, len(t.links))
|
|
|
|
for _, link := range t.links {
|
|
links = append(links, link)
|
|
}
|
|
|
|
t.RUnlock()
|
|
|
|
return links
|
|
}
|
|
|
|
func (t *tun) String() string {
|
|
return "mucp"
|
|
}
|