Back to blogDecentralized Local Sharing: How WebRTC Powers LAN-Drop
GoWebRTCWebSocketsDecentralized

Decentralized Local Sharing: How WebRTC Powers LAN-Drop

July 20, 20263 min read

"Creating a zero-install AirDrop alternative for local networks using WebRTC data channels and Go coordination servers."

Sharing files or text between your laptop and phone should be instantaneous. Unfortunately, standard workflows often involve uploading files to cloud storage, sending messages to ourselves on chat apps, or installing heavy cross-platform software.

We can solve this problem by leveraging WebRTC (Web Real-Time Communication) to establish direct, peer-to-peer data channels between browsers on the same local network, coordinated by a lightweight Go signaling server.

Here is a breakdown of how LAN-Drop establishes peer-to-peer connections directly within standard web browsers.

The WebRTC Peer Lifecycle

To initiate direct data transfers, two devices must exchange metadata (Session Description Protocol - SDP) and network routes (ICE Candidates). Because they don’t know each other’s local IPs initially, a central Signaling Server bridges the handshake.

[Device A: Browser] <── WebSocket (SDP/ICE) ──> [Go Signaling Server] <── WebSocket (SDP/ICE) ──> [Device B: Browser]
         │                                                                                                 │
         └────────────────────────── P2P Encrypted Data Channel (WebRTC) ──────────────────────────────────┘

Once the SDP handshake finishes, the browsers connect directly over the local network (LAN) and the signaling server goes idle.

The Go Signaling Server

The signaling server must be extremely lightweight. In Go, we can write a WebSocket relay handler using the gorilla/websocket library:

package main

import (
	"log"
	"net/http"
	"sync"
	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool { return true },
}

type SignalServer struct {
	mu      sync.Mutex
	clients map[string]*websocket.Conn
}

func (s *SignalServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("Upgrade Error:", err)
		return
	}
	defer conn.Close()

	clientId := r.URL.Query().Get("id")
	
	s.mu.Lock()
	s.clients[clientId] = conn
	s.mu.Unlock()

	log.Printf("Client %s connected to signal pool\n", clientId)

	for {
		// Read message from client (e.g., offer, answer, or ICE candidate)
		_, msg, err := conn.ReadMessage()
		if err != nil {
			break
		}

		// Relay signal data to target device...
		s.relayMessage(clientId, msg)
	}

	s.mu.Lock()
	delete(s.clients, clientId)
	s.mu.Unlock()
}

Transferring Files Over WebRTC Data Channels

Inside the browser, we read the file as an ArrayBuffer and stream it through a WebRTC RTCDataChannel in small, buffered chunks (typically 16KB to 64KB) to avoid memory overflow:

const peerConnection = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});

const dataChannel = peerConnection.createDataChannel("fileTransfer");

dataChannel.onopen = () => {
  console.log("Direct P2P Data Channel opened!");
};

// Stream file in chunks
function sendFile(file) {
  const chunkSize = 16384; // 16KB
  const reader = new FileReader();
  let offset = 0;

  reader.onload = (e) => {
    dataChannel.send(e.target.result);
    offset += e.target.result.byteLength;
    if (offset < file.size) {
      readNextChunk();
    } else {
      console.log("File sent completely!");
    }
  };

  const readNextChunk = () => {
    const slice = file.slice(offset, offset + chunkSize);
    reader.readAsArrayBuffer(slice);
  };

  readNextChunk();
}

Conclusion

WebRTC combined with a lightweight Go signaling server makes local sharing fast, secure, and decentralized. Because data streams directly between devices on the same router, transfer speeds are only limited by your local router’s hardware bandwidth, and your private files never hit the public cloud.