Skip to content

Commit c67d5f7

Browse files
committed
feat: Reconnecting mmar clients
This change adds the ability for the mmar client to attempt to reconnect if there is ever a disconnection of the tunnel. Which allows for the persistance of the subdomain as long as the mmar client is running. Once the mmar client is shutdown and then restarted, a new subdomain will be issued.
1 parent 657f683 commit c67d5f7

File tree

4 files changed

+98
-8
lines changed

4 files changed

+98
-8
lines changed

constants/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
ID_LENGTH = 6
3030

3131
MAX_TUNNELS_PER_IP = 5
32+
TUNNEL_RECONNECT_TIMEOUT = 3
3233
GRACEFUL_SHUTDOWN_TIMEOUT = 3
3334
TUNNEL_CREATE_TIMEOUT = 3
3435
REQ_BODY_READ_CHUNK_TIMEOUT = 3

internal/client/main.go

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type MmarClient struct {
3232
// Tunnel to Server
3333
protocol.Tunnel
3434
ConfigOptions
35+
subdomain string
3536
}
3637

3738
func (mc *MmarClient) localizeRequest(request *http.Request) {
@@ -107,6 +108,28 @@ func (mc *MmarClient) handleRequestMessage(tunnelMsg protocol.TunnelMessage) {
107108
}
108109
}
109110

111+
// Keep attempting to reconnect the existing tunnel until successful
112+
func (mc *MmarClient) reconnectTunnel(ctx context.Context) {
113+
for {
114+
// If context is cancelled, do not reconnect
115+
if errors.Is(ctx.Err(), context.Canceled) {
116+
return
117+
}
118+
logger.Log(constants.DEFAULT_COLOR, "Attempting to reconnect...")
119+
conn, err := net.DialTimeout(
120+
"tcp",
121+
fmt.Sprintf("%s:%s", mc.ConfigOptions.TunnelHost, mc.ConfigOptions.TunnelTcpPort),
122+
constants.TUNNEL_CREATE_TIMEOUT*time.Second,
123+
)
124+
if err != nil {
125+
time.Sleep(constants.TUNNEL_RECONNECT_TIMEOUT * time.Second)
126+
continue
127+
}
128+
mc.Tunnel.Conn = conn
129+
break
130+
}
131+
}
132+
110133
func (mc *MmarClient) ProcessTunnelMessages(ctx context.Context) {
111134
for {
112135
select {
@@ -115,21 +138,38 @@ func (mc *MmarClient) ProcessTunnelMessages(ctx context.Context) {
115138
default:
116139
tunnelMsg, err := mc.ReceiveMessage()
117140
if err != nil {
118-
if errors.Is(err, io.EOF) {
119-
logger.Log(constants.DEFAULT_COLOR, "Tunnel connection closed from Server. Exiting...")
120-
os.Exit(0)
121-
} else if errors.Is(err, net.ErrClosed) {
122-
logger.Log(constants.DEFAULT_COLOR, "Tunnel connection disconnected from Server. Exiting...")
123-
os.Exit(0)
141+
// If the context was cancelled just return
142+
if errors.Is(ctx.Err(), context.Canceled) {
143+
return
124144
} else if errors.Is(err, os.ErrDeadlineExceeded) {
125145
continue
126146
}
127-
log.Fatalf("Failed to receive message from server tunnel: %v", err)
147+
148+
logger.Log(constants.DEFAULT_COLOR, "Tunnel connection disconnected.")
149+
150+
// Keep trying to reconnect
151+
mc.reconnectTunnel(ctx)
152+
153+
continue
128154
}
129155

130156
switch tunnelMsg.MsgType {
131157
case protocol.CLIENT_CONNECT:
132-
logger.LogTunnelCreated(string(tunnelMsg.MsgData), mc.TunnelHost, mc.TunnelHttpPort, mc.LocalPort)
158+
tunnelSubdomain := string(tunnelMsg.MsgData)
159+
// If there is an existing subdomain, that means we are reconnecting with an
160+
// existing mmar client, try to reclaim the same subdomain
161+
if mc.subdomain != "" {
162+
reconnectMsg := protocol.TunnelMessage{MsgType: protocol.CLIENT_RECLAIM_SUBDOMAIN, MsgData: []byte(tunnelSubdomain + ":" + mc.subdomain)}
163+
mc.subdomain = ""
164+
if err := mc.SendMessage(reconnectMsg); err != nil {
165+
logger.Log(constants.DEFAULT_COLOR, "Tunnel failed to reconnect. Exiting...")
166+
os.Exit(0)
167+
}
168+
continue
169+
} else {
170+
mc.subdomain = tunnelSubdomain
171+
}
172+
logger.LogTunnelCreated(tunnelSubdomain, mc.TunnelHost, mc.TunnelHttpPort, mc.LocalPort)
133173
case protocol.CLIENT_TUNNEL_LIMIT:
134174
limit := logger.ColorLogStr(
135175
constants.RED,
@@ -177,6 +217,7 @@ func Run(config ConfigOptions) {
177217
mmarClient := MmarClient{
178218
protocol.Tunnel{Conn: conn},
179219
config,
220+
"",
180221
}
181222

182223
// Create context to cancel running gouroutines when shutting down

internal/protocol/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const (
2020
REQUEST = uint8(iota + 1)
2121
RESPONSE
2222
CLIENT_CONNECT
23+
CLIENT_RECLAIM_SUBDOMAIN
2324
CLIENT_DISCONNECT
2425
CLIENT_TUNNEL_LIMIT
2526
LOCALHOST_NOT_RUNNING

internal/server/main.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"os"
1717
"os/signal"
1818
"slices"
19+
"strings"
1920
"sync"
2021
"time"
2122

@@ -465,6 +466,52 @@ func (ms *MmarServer) processTunnelMessages(ct *ClientTunnel) {
465466
case protocol.CLIENT_DISCONNECT:
466467
ms.closeClientTunnel(ct)
467468
return
469+
case protocol.CLIENT_RECLAIM_SUBDOMAIN:
470+
newAndExistingIDs := strings.Split(string(tunnelMsg.MsgData), ":")
471+
newId := newAndExistingIDs[0]
472+
existingId := newAndExistingIDs[1]
473+
474+
// Check if the subdomain has already been taken
475+
_, ok := ms.clients[existingId]
476+
if ok {
477+
// if so, close the tunnel, so the user can create a new one
478+
ms.closeClientTunnel(ct)
479+
return
480+
}
481+
482+
ct.Tunnel.Id = existingId
483+
484+
// Add existing client tunnel to clients
485+
ms.clients[existingId] = *ct
486+
487+
// Remove newId tunnel from clients
488+
delete(ms.clients, newId)
489+
490+
// Update the tunnels for the IP
491+
clientIP := utils.ExtractIP(ct.Conn.RemoteAddr().String())
492+
newIdIndex := slices.Index(ms.tunnelsPerIP[clientIP], newId)
493+
if newIdIndex == -1 {
494+
ms.tunnelsPerIP[clientIP] = append(ms.tunnelsPerIP[clientIP], existingId)
495+
} else {
496+
ms.tunnelsPerIP[clientIP][newIdIndex] = existingId
497+
}
498+
499+
connMessage := protocol.TunnelMessage{MsgType: protocol.CLIENT_CONNECT, MsgData: []byte(existingId)}
500+
if err := ct.SendMessage(connMessage); err != nil {
501+
logger.Log(constants.DEFAULT_COLOR, fmt.Sprintf("Failed to send unique ID msg to client: %v", err))
502+
ms.closeClientTunnel(ct)
503+
return
504+
}
505+
506+
logger.Log(
507+
constants.DEFAULT_COLOR,
508+
fmt.Sprintf(
509+
"[%s] Tunnel reclaimed: %s -> %s",
510+
newId,
511+
ct.Conn.RemoteAddr().String(),
512+
existingId,
513+
),
514+
)
468515
}
469516
}
470517
}

0 commit comments

Comments
 (0)