Files
Jaime Pillora 44310b6566 Enforce auth ACL on tunnel channels
Previously, authfile ACL restrictions were only checked during the
initial config handshake. This adds ACL enforcement at the tunnel
layer when processing SSH channel requests, ensuring that each
outbound connection is validated against the user's allowed addresses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 20:47:37 +11:00

172 lines
4.6 KiB
Go

package chserver
import (
"net/http"
"strings"
"sync/atomic"
"time"
chshare "github.com/jpillora/chisel/share"
"github.com/jpillora/chisel/share/cnet"
"github.com/jpillora/chisel/share/settings"
"github.com/jpillora/chisel/share/tunnel"
"golang.org/x/crypto/ssh"
"golang.org/x/sync/errgroup"
)
// handleClientHandler is the main http websocket handler for the chisel server
func (s *Server) handleClientHandler(w http.ResponseWriter, r *http.Request) {
//websockets upgrade AND has chisel prefix
upgrade := strings.ToLower(r.Header.Get("Upgrade"))
protocol := r.Header.Get("Sec-WebSocket-Protocol")
if upgrade == "websocket" {
if protocol == chshare.ProtocolVersion {
s.handleWebsocket(w, r)
return
}
//print into server logs and silently fall-through
s.Infof("ignored client connection using protocol '%s', expected '%s'",
protocol, chshare.ProtocolVersion)
}
//proxy target was provided
if s.reverseProxy != nil {
s.reverseProxy.ServeHTTP(w, r)
return
}
//no proxy defined, provide access to health/version checks
switch r.URL.Path {
case "/health":
w.Write([]byte("OK\n"))
return
case "/version":
w.Write([]byte(chshare.BuildVersion))
return
}
//missing :O
w.WriteHeader(404)
w.Write([]byte("Not found"))
}
// handleWebsocket is responsible for handling the websocket connection
func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
id := atomic.AddInt32(&s.sessCount, 1)
l := s.Fork("session#%d", id)
wsConn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
l.Debugf("Failed to upgrade (%s)", err)
return
}
conn := cnet.NewWebSocketConn(wsConn)
// perform SSH handshake on net.Conn
l.Debugf("Handshaking with %s...", req.RemoteAddr)
sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)
if err != nil {
s.Debugf("Failed to handshake (%s)", err)
return
}
// pull the users from the session map
var user *settings.User
if s.users.Len() > 0 {
sid := string(sshConn.SessionID())
u, ok := s.sessions.Get(sid)
if !ok {
panic("bug in ssh auth handler")
}
user = u
s.sessions.Del(sid)
}
// chisel server handshake (reverse of client handshake)
// verify configuration
l.Debugf("Verifying configuration")
// wait for request, with timeout
var r *ssh.Request
select {
case r = <-reqs:
case <-time.After(settings.EnvDuration("CONFIG_TIMEOUT", 10*time.Second)):
l.Debugf("Timeout waiting for configuration")
sshConn.Close()
return
}
failed := func(err error) {
l.Debugf("Failed: %s", err)
r.Reply(false, []byte(err.Error()))
}
if r.Type != "config" {
failed(s.Errorf("expecting config request"))
return
}
c, err := settings.DecodeConfig(r.Payload)
if err != nil {
failed(s.Errorf("invalid config"))
return
}
//print if client and server versions dont match
cv := strings.TrimPrefix(c.Version, "v")
if cv == "" {
cv = "<unknown>"
}
sv := strings.TrimPrefix(chshare.BuildVersion, "v")
if cv != sv {
l.Infof("Client version (%s) differs from server version (%s)", cv, sv)
}
//validate remotes
for _, r := range c.Remotes {
//if user is provided, ensure they have
//access to the desired remotes
if user != nil {
addr := r.UserAddr()
if !user.HasAccess(addr) {
failed(s.Errorf("access to '%s' denied", addr))
return
}
}
//confirm reverse tunnels are allowed
if r.Reverse && !s.config.Reverse {
l.Debugf("Denied reverse port forwarding request, please enable --reverse")
failed(s.Errorf("Reverse port forwaring not enabled on server"))
return
}
//confirm reverse tunnel is available
if r.Reverse && !r.CanListen() {
failed(s.Errorf("Server cannot listen on %s", r.String()))
return
}
}
//successfuly validated config!
r.Reply(true, nil)
//tunnel per ssh connection
tunnelConfig := tunnel.Config{
Logger: l,
Inbound: s.config.Reverse,
Outbound: true, //server always accepts outbound
Socks: s.config.Socks5,
KeepAlive: s.config.KeepAlive,
}
//enforce ACL on every channel, not just the initial config
if user != nil {
tunnelConfig.ACL = user.HasAccess
}
tunnel := tunnel.New(tunnelConfig)
//bind
eg, ctx := errgroup.WithContext(req.Context())
eg.Go(func() error {
//connected, handover ssh connection for tunnel to use, and block
return tunnel.BindSSH(ctx, sshConn, reqs, chans)
})
eg.Go(func() error {
//connected, setup reversed-remotes?
serverInbound := c.Remotes.Reversed(true)
if len(serverInbound) == 0 {
return nil
}
//block
return tunnel.BindRemotes(ctx, serverInbound)
})
err = eg.Wait()
if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
l.Debugf("Closed connection (%s)", err)
} else {
l.Debugf("Closed connection")
}
}