Skip to content
16 changes: 16 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type OlmConfig struct {
OverrideDNS bool `json:"overrideDNS"`
TunnelDNS bool `json:"tunnelDNS"`
DisableRelay bool `json:"disableRelay"`
WebSocketRelay bool `json:"websocketRelay"`
// DoNotCreateNewClient bool `json:"doNotCreateNewClient"`

// Parsed values (not in JSON)
Expand Down Expand Up @@ -110,6 +111,7 @@ func DefaultConfig() *OlmConfig {
config.sources["overrideDNS"] = string(SourceDefault)
config.sources["tunnelDNS"] = string(SourceDefault)
config.sources["disableRelay"] = string(SourceDefault)
config.sources["websocketRelay"] = string(SourceDefault)
// config.sources["doNotCreateNewClient"] = string(SourceDefault)

return config
Expand Down Expand Up @@ -269,6 +271,10 @@ func loadConfigFromEnv(config *OlmConfig) {
config.DisableRelay = true
config.sources["disableRelay"] = string(SourceEnv)
}
if val := os.Getenv("WEBSOCKET_RELAY"); val == "true" {
config.WebSocketRelay = true
config.sources["websocketRelay"] = string(SourceEnv)
}
if val := os.Getenv("TUNNEL_DNS"); val == "true" {
config.TunnelDNS = true
config.sources["tunnelDNS"] = string(SourceEnv)
Expand Down Expand Up @@ -303,6 +309,7 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
"disableHolepunch": config.DisableHolepunch,
"overrideDNS": config.OverrideDNS,
"disableRelay": config.DisableRelay,
"websocketRelay": config.WebSocketRelay,
"tunnelDNS": config.TunnelDNS,
// "doNotCreateNewClient": config.DoNotCreateNewClient,
}
Expand All @@ -327,6 +334,7 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
serviceFlags.BoolVar(&config.DisableHolepunch, "disable-holepunch", config.DisableHolepunch, "Disable hole punching")
serviceFlags.BoolVar(&config.OverrideDNS, "override-dns", config.OverrideDNS, "When enabled, the client uses custom DNS servers to resolve internal resources and aliases. This overrides your system's default DNS settings. Queries that cannot be resolved as a Pangolin resource will be forwarded to your configured Upstream DNS Server. (default false)")
serviceFlags.BoolVar(&config.DisableRelay, "disable-relay", config.DisableRelay, "Disable relay connections")
serviceFlags.BoolVar(&config.WebSocketRelay, "websocket-relay", config.WebSocketRelay, "Force WebSocket relay transport without waiting for fallback")
serviceFlags.BoolVar(&config.TunnelDNS, "tunnel-dns", config.TunnelDNS, "When enabled, DNS queries are routed through the tunnel for remote resolution. To ensure queries are tunneled correctly, you must define the DNS server as a Pangolin resource and enter its address as an Upstream DNS Server. (default false)")
// serviceFlags.BoolVar(&config.DoNotCreateNewClient, "do-not-create-new-client", config.DoNotCreateNewClient, "Do not create new client")

Expand Down Expand Up @@ -403,6 +411,9 @@ func loadConfigFromCLI(config *OlmConfig, args []string) (bool, bool, error) {
if config.DisableRelay != origValues["disableRelay"].(bool) {
config.sources["disableRelay"] = string(SourceCLI)
}
if config.WebSocketRelay != origValues["websocketRelay"].(bool) {
config.sources["websocketRelay"] = string(SourceCLI)
}
if config.TunnelDNS != origValues["tunnelDNS"].(bool) {
config.sources["tunnelDNS"] = string(SourceCLI)
}
Expand Down Expand Up @@ -530,6 +541,10 @@ func mergeConfigs(dest, src *OlmConfig) {
dest.DisableRelay = src.DisableRelay
dest.sources["disableRelay"] = string(SourceFile)
}
if src.WebSocketRelay {
dest.WebSocketRelay = src.WebSocketRelay
dest.sources["websocketRelay"] = string(SourceFile)
}
// if src.DoNotCreateNewClient {
// dest.DoNotCreateNewClient = src.DoNotCreateNewClient
// dest.sources["doNotCreateNewClient"] = string(SourceFile)
Expand Down Expand Up @@ -621,6 +636,7 @@ func (c *OlmConfig) ShowConfig() {
fmt.Printf(" override-dns = %v [%s]\n", c.OverrideDNS, getSource("overrideDNS"))
fmt.Printf(" tunnel-dns = %v [%s]\n", c.TunnelDNS, getSource("tunnelDNS"))
fmt.Printf(" disable-relay = %v [%s]\n", c.DisableRelay, getSource("disableRelay"))
fmt.Printf(" websocket-relay = %v [%s]\n", c.WebSocketRelay, getSource("websocketRelay"))
// fmt.Printf(" do-not-create-new-client = %v [%s]\n", c.DoNotCreateNewClient, getSource("doNotCreateNewClient"))
if c.TlsClientCert != "" {
fmt.Printf(" tls-cert = %s [%s]\n", c.TlsClientCert, getSource("tlsClientCert"))
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ func runOlmMainWithArgs(ctx context.Context, cancel context.CancelFunc, signalCt
OrgID: config.OrgID,
OverrideDNS: config.OverrideDNS,
DisableRelay: config.DisableRelay,
WebSocketRelay: config.WebSocketRelay,
EnableUAPI: true,
}
go olm.StartTunnel(tunnelConfig)
Expand Down
36 changes: 33 additions & 3 deletions olm/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
logger.Info("Got new message. Closing existing tunnel!")
o.dev.Close()
}
o.relaySwitchMu.Lock()
if o.wsRelayBind != nil {
_ = o.wsRelayBind.Shutdown()
o.wsRelayBind = nil
}
o.wssRelayActive = false
o.relaySwitchMu.Unlock()

jsonData, err := json.Marshal(msg.Data)
if err != nil {
Expand Down Expand Up @@ -96,8 +103,21 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
o.middleDev = olmDevice.NewMiddleDevice(o.tdev)

wgLogger := logger.GetLogger().GetWireGuardLogger("wireguard: ")
// Use filtered device instead of raw TUN device
o.dev = device.NewDevice(o.middleDev, o.sharedBind, (*device.Logger)(wgLogger))
// Use filtered device instead of raw TUN device.
if o.tunnelConfig.WebSocketRelay {
relayURL, err := o.resolveRelayTunnelURL()
if err != nil {
logger.Error("Forced websocket relay mode requires relay URL: %v", err)
return
}
o.wsRelayBind = NewWebSocketRelayBind(relayURL, nil)
o.wssRelayActive = true
o.dev = device.NewDevice(o.middleDev, o.wsRelayBind, (*device.Logger)(wgLogger))
logger.Info("Using websocket relay bind for forced relay mode")
} else {
o.dev = device.NewDevice(o.middleDev, o.sharedBind, (*device.Logger)(wgLogger))
o.wssRelayActive = false
}

if o.tunnelConfig.EnableUAPI {
fileUAPI, err := func() (*os.File, error) {
Expand Down Expand Up @@ -135,7 +155,12 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
}

if err = o.dev.Up(); err != nil {
logger.Error("Failed to bring up WireGuard device: %v", err)
logger.Error(
"Failed to bring up WireGuard device: %v (holepunch=%v)",
err,
o.tunnelConfig.Holepunch,
)
return
}

// Extract interface IP (strip CIDR notation if present)
Expand Down Expand Up @@ -246,6 +271,11 @@ func (o *Olm) handleConnect(msg websocket.WSMessage) {
o.apiServer.SetRegistered(true)

o.registered = true
if o.tunnelConfig.WebSocketRelay {
logger.Debug("Forced websocket relay mode enabled; runtime relay switch timer disabled")
} else {
logger.Debug("WebSocket relay forced mode disabled; keeping UDP transport until fallback triggers")
}

// Start ping monitor now that we are registered and connected
o.websocket.StartPingMonitor()
Expand Down
83 changes: 71 additions & 12 deletions olm/olm.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"sync"
"time"
Expand Down Expand Up @@ -41,6 +42,7 @@ type Olm struct {
tdev tun.Device
middleDev *olmDevice.MiddleDevice
sharedBind *bind.SharedBind
wsRelayBind *WebSocketRelayBind

dnsProxy *dns.DNSProxy
apiServer *api.API
Expand Down Expand Up @@ -68,13 +70,22 @@ type Olm struct {
stopRegister func()
updateRegister func(newData any)

stopPeerSends map[string]func()
stopPeerInits map[string]func()
stopPeerSends map[string]func()
stopPeerInits map[string]func()
jitPendingSites map[int]string // siteId -> chainId for in-flight JIT requests
peerSendMu sync.Mutex
peerSendMu sync.Mutex

// WaitGroup to track tunnel lifecycle
tunnelWg sync.WaitGroup

// Cached websocket relay URL for WSS / TCP fallback relay modes
relayTunnelURL string
relayURLMu sync.RWMutex
relayTimerMu sync.Mutex
relayTimers map[int]*time.Timer
relaySwitchMu sync.Mutex
relaySwitchTimer *time.Timer
wssRelayActive bool
}

// getPeerManager safely returns the current peerManager under a read-lock.
Expand Down Expand Up @@ -138,6 +149,21 @@ func generateChainId() string {
return hex.EncodeToString(b)
}

func buildRelayTunnelURL(baseEndpointWss, token, userToken string) (string, error) {
u, err := url.Parse(baseEndpointWss)
if err != nil {
return "", fmt.Errorf("invalid relay endpoint wss URL: %w", err)
}

q := u.Query()
q.Set("token", token)
if userToken != "" {
q.Set("userToken", userToken)
}
u.RawQuery = q.Encode()
return u.String(), nil
}

func Init(ctx context.Context, config OlmConfig) (*Olm, error) {
logger.GetLogger().SetLevel(util.ParseLogLevel(config.LogLevel))

Expand Down Expand Up @@ -188,13 +214,14 @@ func Init(ctx context.Context, config OlmConfig) (*Olm, error) {
apiServer.SetAgent(config.Agent)

newOlm := &Olm{
logFile: logFile,
olmCtx: ctx,
apiServer: apiServer,
olmConfig: config,
logFile: logFile,
olmCtx: ctx,
apiServer: apiServer,
olmConfig: config,
stopPeerSends: make(map[string]func()),
stopPeerInits: make(map[string]func()),
jitPendingSites: make(map[int]string),
relayTimers: make(map[int]*time.Timer),
}

newOlm.registerAPICallbacks()
Expand Down Expand Up @@ -457,10 +484,10 @@ func (o *Olm) StartTunnel(config TunnelConfig) {
}

if o.stopRegister == nil {
logger.Debug("Sending registration message to server with public key: %s and relay: %v", publicKey, !config.Holepunch)
logger.Debug("Sending registration message to server with public key: %s and relay: false", publicKey)
o.stopRegister, o.updateRegister = o.websocket.SendMessageInterval("olm/wg/register", map[string]any{
"publicKey": publicKey.String(),
"relay": !config.Holepunch,
"relay": false,
"olmVersion": o.olmConfig.Version,
"olmAgent": o.olmConfig.Agent,
"orgId": config.OrgID,
Expand All @@ -479,9 +506,33 @@ func (o *Olm) StartTunnel(config TunnelConfig) {
})

o.websocket.OnTokenUpdate(func(token string, exitNodes []websocket.ExitNode) {
// Check if tunnel is still running and hole punch manager exists
if !o.tunnelRunning || o.holePunchManager == nil {
logger.Debug("Tunnel stopped or hole punch manager nil, ignoring token update")
// Check if tunnel is still running
if !o.tunnelRunning {
logger.Debug("Tunnel stopped, ignoring token update")
return
}

var relayEndpointWss string
for i := range exitNodes {
relayEndpointWss = exitNodes[i].RelayEndpointWss
if relayEndpointWss != "" {
break
}
}
if relayEndpointWss != "" {
relayURL, err := buildRelayTunnelURL(relayEndpointWss, token, userToken)
if err != nil {
logger.Warn("Failed to build relay tunnel URL: %v", err)
} else {
o.relayURLMu.Lock()
o.relayTunnelURL = relayURL
o.relayURLMu.Unlock()
logger.Debug("Cached websocket relay tunnel URL for WSS relay bind")
}
}

if o.holePunchManager == nil {
logger.Debug("Hole punch manager nil, ignoring token update")
return
}

Expand Down Expand Up @@ -563,6 +614,10 @@ func (o *Olm) Close() {
o.stopRegister()
o.stopRegister = nil
}
o.cancelRelaySwitchTimer()
o.relaySwitchMu.Lock()
o.wssRelayActive = false
o.relaySwitchMu.Unlock()

// Stop all pending peer init and send senders before closing websocket
o.peerSendMu.Lock()
Expand Down Expand Up @@ -652,10 +707,14 @@ func (o *Olm) Close() {
// Now close WireGuard device - its TUN reader should have exited by now
// This will call sharedBind.Close() which releases WireGuard's reference
if o.dev != nil {
if o.wsRelayBind != nil {
_ = o.wsRelayBind.Shutdown()
}
logger.Debug("Closing WireGuard device")
o.dev.Close()
o.dev = nil
}
o.wsRelayBind = nil

// Release the hole punch reference to the shared bind (WireGuard already
// released its reference via dev.Close())
Expand Down
Loading