mirror of
https://github.com/fosrl/newt.git
synced 2026-10-28 22:49:09 +01:00
@@ -1,7 +1,10 @@
|
||||
package browsergateway
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Forwarding buffer size. RDP graphics traffic is bursty and TLS records cap
|
||||
@@ -9,6 +12,24 @@ import (
|
||||
// without wasting memory per session.
|
||||
const forwardBufSize = 64 * 1024
|
||||
|
||||
// ListenPort is the port the browser gateway HTTP server listens on inside the
|
||||
// WireGuard netstack. This is a fixed value shared between newt and pangolin.
|
||||
const ListenPort = 8082
|
||||
|
||||
// HardcodedAuthToken is a temporary shared secret used during development.
|
||||
// TODO: replace with a per-session token negotiated with pangolin.
|
||||
const HardcodedAuthToken = "pangolin-browser-gateway-dev"
|
||||
|
||||
// Target represents an allowed proxy destination for the browser gateway.
|
||||
// Only connections whose (Type, Destination, DestinationPort) match a
|
||||
// registered Target will be forwarded; all others are rejected.
|
||||
type Target struct {
|
||||
ID int
|
||||
Type string // "rdp" | "ssh" | "vnc"
|
||||
Destination string
|
||||
DestinationPort int
|
||||
}
|
||||
|
||||
// Config holds the configuration for a Gateway.
|
||||
type Config struct {
|
||||
// AuthToken is the shared secret required by RDP clients in the RDCleanPath
|
||||
@@ -25,6 +46,11 @@ type Config struct {
|
||||
type Gateway struct {
|
||||
authToken string
|
||||
nativeSSH *NativeSSHConfig
|
||||
|
||||
mu sync.RWMutex
|
||||
targets map[int]Target // keyed by Target.ID
|
||||
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// New creates a new Gateway from the provided Config.
|
||||
@@ -32,9 +58,60 @@ func New(cfg Config) *Gateway {
|
||||
return &Gateway{
|
||||
authToken: cfg.AuthToken,
|
||||
nativeSSH: cfg.NativeSSH,
|
||||
targets: make(map[int]Target),
|
||||
}
|
||||
}
|
||||
|
||||
// SetTargets replaces the entire allowed-destination list atomically.
|
||||
func (g *Gateway) SetTargets(targets []Target) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.targets = make(map[int]Target, len(targets))
|
||||
for _, t := range targets {
|
||||
g.targets[t.ID] = t
|
||||
}
|
||||
}
|
||||
|
||||
// AddTarget adds or updates a single allowed destination.
|
||||
func (g *Gateway) AddTarget(t Target) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
g.targets[t.ID] = t
|
||||
}
|
||||
|
||||
// RemoveTarget removes an allowed destination by its ID.
|
||||
func (g *Gateway) RemoveTarget(id int) {
|
||||
g.mu.Lock()
|
||||
defer g.mu.Unlock()
|
||||
delete(g.targets, id)
|
||||
}
|
||||
|
||||
// isAllowed reports whether a connection to (targetType, host, port) is
|
||||
// permitted by the current target list.
|
||||
func (g *Gateway) isAllowed(targetType, host string, port int) bool {
|
||||
g.mu.RLock()
|
||||
defer g.mu.RUnlock()
|
||||
for _, t := range g.targets {
|
||||
if t.Type == targetType && t.Destination == host && t.DestinationPort == port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Start serves the browser gateway HTTP server on the provided listener.
|
||||
// It returns nil when the listener is closed (normal shutdown).
|
||||
func (g *Gateway) Start(ln net.Listener) error {
|
||||
mux := http.NewServeMux()
|
||||
g.RegisterHandlers(mux)
|
||||
g.server = &http.Server{Handler: mux}
|
||||
err := g.server.Serve(ln)
|
||||
if errors.Is(err, net.ErrClosed) || errors.Is(err, http.ErrServerClosed) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// RegisterHandlers registers the /rdp, /ssh, and /vnc routes on mux.
|
||||
func (g *Gateway) RegisterHandlers(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/rdp", g.HandleRDP)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
@@ -68,6 +69,13 @@ func (g *Gateway) serveSession(ctx context.Context, ws *websocket.Conn) error {
|
||||
target = net.JoinHostPort(target, "3389")
|
||||
}
|
||||
|
||||
// Validate destination against the registered target allowlist.
|
||||
rdpHost, rdpPortStr, _ := net.SplitHostPort(target)
|
||||
rdpPort, _ := strconv.Atoi(rdpPortStr)
|
||||
if !g.isAllowed("rdp", rdpHost, rdpPort) {
|
||||
return fmt.Errorf("RDP destination %s is not in the allowed target list", target)
|
||||
}
|
||||
|
||||
log.Printf("Connecting to RDP server %s", target)
|
||||
|
||||
// -- Open TCP connection to the destination RDP server --
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
@@ -56,6 +57,11 @@ func (g *Gateway) HandleSSH(w http.ResponseWriter, r *http.Request) {
|
||||
if port == "" {
|
||||
port = "22"
|
||||
}
|
||||
sshPort, _ := strconv.Atoi(port)
|
||||
if !g.isAllowed("ssh", host, sshPort) {
|
||||
http.Error(w, "destination not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
target = net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
@@ -40,6 +41,11 @@ func (g *Gateway) handleVNC(w http.ResponseWriter, r *http.Request) {
|
||||
if port == "" {
|
||||
port = "5900"
|
||||
}
|
||||
vncPort, _ := strconv.Atoi(port)
|
||||
if !g.isAllowed("vnc", host, vncPort) {
|
||||
http.Error(w, "destination not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
target := net.JoinHostPort(host, port)
|
||||
|
||||
// Accept the WebSocket. noVNC negotiates the "binary" subprotocol;
|
||||
|
||||
Reference in New Issue
Block a user