mirror of
https://github.com/fosrl/newt.git
synced 2026-09-01 11:41:55 +02:00
Support per target auth token
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package browsergateway
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -14,26 +16,25 @@ 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"
|
||||
// Targets do not overlap with this port because they start at 40000.
|
||||
const ListenPort = 39999
|
||||
|
||||
// Target represents an allowed proxy destination for the browser gateway.
|
||||
// Only connections whose (Type, Destination, DestinationPort) match a
|
||||
// Only connections whose (Type, Destination, DestinationPort, AuthToken) 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
|
||||
AuthToken string // per-target secret; must match the token supplied by the client
|
||||
}
|
||||
|
||||
// Config holds the configuration for a Gateway.
|
||||
type Config struct {
|
||||
// AuthToken is the shared secret required by RDP clients in the RDCleanPath
|
||||
// ProxyAuth field, and by SSH clients as the authToken query parameter.
|
||||
// AuthToken is used only for NativeSSH mode (which has no external target
|
||||
// to match against). For all proxy targets (RDP/SSH/VNC), auth tokens are
|
||||
// stored per-Target and validated by isAllowed.
|
||||
AuthToken string
|
||||
// NativeSSH, when non-nil, configures a local PTY/shell SSH mode instead
|
||||
// of proxying to an external SSH server.
|
||||
@@ -86,14 +87,15 @@ func (g *Gateway) RemoveTarget(id int) {
|
||||
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 {
|
||||
// isAllowed reports whether a connection to (targetType, host, port) with the
|
||||
// given authToken is permitted. The token is compared against the per-target
|
||||
// AuthToken using constant-time comparison to prevent timing attacks.
|
||||
func (g *Gateway) isAllowed(targetType, host string, port int, authToken string) 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 subtle.ConstantTimeCompare([]byte(authToken), []byte(t.AuthToken)) == 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
@@ -106,7 +108,11 @@ func (g *Gateway) Start(ln net.Listener) error {
|
||||
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) {
|
||||
if err == nil ||
|
||||
errors.Is(err, net.ErrClosed) ||
|
||||
errors.Is(err, http.ErrServerClosed) ||
|
||||
strings.Contains(err.Error(), "use of closed") ||
|
||||
strings.Contains(err.Error(), "invalid state") {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -114,7 +120,7 @@ func (g *Gateway) Start(ln net.Listener) error {
|
||||
|
||||
// RegisterHandlers registers the /rdp, /ssh, and /vnc routes on mux.
|
||||
func (g *Gateway) RegisterHandlers(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/rdp", g.HandleRDP)
|
||||
mux.HandleFunc("/ssh", g.HandleSSH)
|
||||
mux.HandleFunc("/vnc", g.handleVNC)
|
||||
mux.HandleFunc("/gateway/rdp", g.HandleRDP)
|
||||
mux.HandleFunc("/gateway/ssh", g.HandleSSH)
|
||||
mux.HandleFunc("/gateway/vnc", g.handleVNC)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package browsergateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
@@ -58,22 +57,18 @@ func (g *Gateway) serveSession(ctx context.Context, ws *websocket.Conn) error {
|
||||
return errors.New("RDCleanPath missing X224 connection PDU")
|
||||
}
|
||||
|
||||
// Constant-time comparison to avoid leaking the expected token via timing.
|
||||
if subtle.ConstantTimeCompare([]byte(pdu.ProxyAuth), []byte(g.authToken)) != 1 {
|
||||
return errors.New("RDCleanPath ProxyAuth token mismatch")
|
||||
}
|
||||
|
||||
target := pdu.Destination
|
||||
// Default port for RDP if not specified.
|
||||
if _, _, splitErr := net.SplitHostPort(target); splitErr != nil {
|
||||
target = net.JoinHostPort(target, "3389")
|
||||
}
|
||||
|
||||
// Validate destination against the registered target allowlist.
|
||||
// Validate destination against the registered target allowlist,
|
||||
// including per-target auth token.
|
||||
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)
|
||||
if !g.isAllowed("rdp", rdpHost, rdpPort, pdu.ProxyAuth) {
|
||||
return fmt.Errorf("RDP destination %s is not in the allowed target list or auth token mismatch", target)
|
||||
}
|
||||
|
||||
log.Printf("Connecting to RDP server %s", target)
|
||||
|
||||
@@ -37,12 +37,7 @@ type sshServerMsg struct {
|
||||
func (g *Gateway) HandleSSH(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// -- Validate auth token from query parameter before upgrading --
|
||||
token := r.URL.Query().Get("authToken")
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(g.authToken)) != 1 {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// In proxy mode we also need host + username from query params.
|
||||
var target, username string
|
||||
@@ -58,11 +53,17 @@ func (g *Gateway) HandleSSH(w http.ResponseWriter, r *http.Request) {
|
||||
port = "22"
|
||||
}
|
||||
sshPort, _ := strconv.Atoi(port)
|
||||
if !g.isAllowed("ssh", host, sshPort) {
|
||||
http.Error(w, "destination not allowed", http.StatusForbidden)
|
||||
if !g.isAllowed("ssh", host, sshPort, token) {
|
||||
http.Error(w, "destination not allowed or auth token mismatch", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
target = net.JoinHostPort(host, port)
|
||||
} else {
|
||||
// Native SSH mode: validate against the global gateway token.
|
||||
if subtle.ConstantTimeCompare([]byte(token), []byte(g.authToken)) != 1 {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ws, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
|
||||
@@ -2,7 +2,6 @@ package browsergateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
@@ -28,10 +27,6 @@ const (
|
||||
// host – VNC backend hostname or IP
|
||||
// port – VNC backend port (default: 5900)
|
||||
func (g *Gateway) handleVNC(w http.ResponseWriter, r *http.Request) {
|
||||
if subtle.ConstantTimeCompare([]byte(r.URL.Query().Get("authToken")), []byte(g.authToken)) != 1 {
|
||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
host := r.URL.Query().Get("host")
|
||||
port := r.URL.Query().Get("port")
|
||||
if host == "" {
|
||||
@@ -42,8 +37,9 @@ func (g *Gateway) handleVNC(w http.ResponseWriter, r *http.Request) {
|
||||
port = "5900"
|
||||
}
|
||||
vncPort, _ := strconv.Atoi(port)
|
||||
if !g.isAllowed("vnc", host, vncPort) {
|
||||
http.Error(w, "destination not allowed", http.StatusForbidden)
|
||||
authToken := r.URL.Query().Get("authToken")
|
||||
if !g.isAllowed("vnc", host, vncPort, authToken) {
|
||||
http.Error(w, "destination not allowed or auth token mismatch", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
target := net.JoinHostPort(host, port)
|
||||
|
||||
6
main.go
6
main.go
@@ -46,6 +46,7 @@ type BrowserGatewayTarget struct {
|
||||
Type string `json:"type"`
|
||||
Destination string `json:"destination"`
|
||||
DestinationPort int `json:"destinationPort"`
|
||||
AuthToken string `json:"authToken"`
|
||||
}
|
||||
|
||||
type WgData struct {
|
||||
@@ -981,12 +982,11 @@ persistent_keepalive_interval=5`, util.FixKey(privateKey.String()), util.FixKey(
|
||||
Type: t.Type,
|
||||
Destination: t.Destination,
|
||||
DestinationPort: t.DestinationPort,
|
||||
AuthToken: t.AuthToken,
|
||||
})
|
||||
}
|
||||
|
||||
browserGateway = browsergateway.New(browsergateway.Config{
|
||||
AuthToken: browsergateway.HardcodedAuthToken,
|
||||
})
|
||||
browserGateway = browsergateway.New(browsergateway.Config{})
|
||||
browserGateway.SetTargets(bgTargets)
|
||||
|
||||
ln, bgErr := tnet.ListenTCP(&net.TCPAddr{Port: browsergateway.ListenPort})
|
||||
|
||||
Reference in New Issue
Block a user