ingress: add unix+tcp:// service for raw stream forwarding over unix sockets

Introduce unixSocketTCPService, a new OriginService that dials a unix
socket and forwards raw bytes bidirectionally via WebSocket, without any
HTTP wrapping.  This is the unix-socket analogue of tcpOverWSService.

A new ingress URL scheme unix+tcp:<path> is recognised during ingress
validation and maps to this service type.  Example config:

  ingress:
    - hostname: ssh.example.com
      service: unix+tcp:/run/sshd.sock

The scheme name unix+tcp mirrors the existing unix+tls modifier pattern:
the suffix describes the transport style, not the application protocol,
so the service works equally well for SSH, RDP, or any other stream-based
protocol whose daemon listens on a unix socket.

The implementation reuses the existing tcpOverWSConnection and
DefaultStreamHandler machinery; the only difference from ssh:// (TCP) is
that the underlying net.Conn is obtained via net.Dial("unix", path).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ivan Kruglov 2026-03-15 10:53:45 +01:00
parent d2a87e9b93
commit 07c2516b3c
3 changed files with 38 additions and 0 deletions

View File

@ -255,6 +255,10 @@ func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginReq
} else if prefix := "unix+tls:"; strings.HasPrefix(r.Service, prefix) {
path := strings.TrimPrefix(r.Service, prefix)
service = &unixSocketPath{path: path, scheme: "https"}
} else if prefix := "unix+tcp:"; strings.HasPrefix(r.Service, prefix) {
// Stream raw bytes (e.g. SSH, RDP protocol) directly into a unix socket without HTTP wrapping
path := strings.TrimPrefix(r.Service, prefix)
service = &unixSocketTCPService{path: path}
} else if prefix := "http_status:"; strings.HasPrefix(r.Service, prefix) {
statusCode, err := strconv.Atoi(strings.TrimPrefix(r.Service, prefix))
if err != nil {

View File

@ -119,3 +119,14 @@ func (o *tcpOverWSService) EstablishConnection(ctx context.Context, dest string,
func (o *socksProxyOverWSService) EstablishConnection(_ context.Context, _ string, _ *zerolog.Logger) (OriginConnection, error) {
return o.conn, nil
}
func (o *unixSocketTCPService) EstablishConnection(ctx context.Context, _ string, _ *zerolog.Logger) (OriginConnection, error) {
conn, err := o.dialer.DialContext(ctx, "unix", o.path)
if err != nil {
return nil, err
}
return &tcpOverWSConnection{
conn: conn,
streamHandler: o.streamHandler,
}, nil
}

View File

@ -46,6 +46,14 @@ type unixSocketPath struct {
transport *http.Transport
}
// unixSocketTCPService is an OriginService that streams raw bytes (e.g. SSH, RDP) directly into a
// unix socket, bypassing HTTP entirely. It is the unix-socket analogue of tcpOverWSService.
type unixSocketTCPService struct {
path string
streamHandler streamHandlerFunc
dialer net.Dialer
}
func (o *unixSocketPath) String() string {
scheme := ""
if o.scheme == "https" {
@ -67,6 +75,21 @@ func (o unixSocketPath) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
func (o *unixSocketTCPService) String() string {
return "unix+tcp:" + o.path
}
func (o *unixSocketTCPService) start(_ *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
o.streamHandler = DefaultStreamHandler
o.dialer.Timeout = cfg.ConnectTimeout.Duration
o.dialer.KeepAlive = cfg.TCPKeepAlive.Duration
return nil
}
func (o unixSocketTCPService) MarshalJSON() ([]byte, error) {
return json.Marshal(o.String())
}
type httpService struct {
url *url.URL
hostHeader string