TLS Bridge

This plugin is used to provide secured TLS tunnels for connections between a Client and a Service via two gateway Traffic Server instances. By configuring the Traffic Server instances the level of security in the tunnel can be easily controlled for all communications across the tunnels.

Description

The tunnel is sustained by two instances of Traffic Server.

hide empty members

cloud "Cloud\nUntrusted\nNetworks" as Cloud
node "Ingress ATS"
node "Peer ATS"

[Client] <--> [Ingress ATS] : Unsecure
[Ingress ATS] <-> [Cloud] : Secure
[Cloud] <-> [Peer ATS] : Secure
[Peer ATS] <-u-> [Service] : Unsecure

[Ingress ATS] ..> [tls_bridge\nPlugin] : Uses

The ingress Traffic Server accepts a connection from the Client. This connection gets intercepted by the TLS Bridge plugin inside Traffic Server. The plugin then makes a TLS connection to the peer Traffic Server using the configured level of security. The original request from the Client to the ingress Traffic Server is then sent to the peer Traffic Server to create a connection from the peer Traffic Server to the Service. After this the Client has a virtual circut to the Service and can use any TCP based communication (including TLS). Effectively the plugin causes the connectivity to work as if the Client had done the CONNECT directly to the peer Traffic Server. Note this means the DNS lookup for the Service is done by the peer Traffic Server, not the ingress Traffic Server.

The plugin is configured with a mapping of Service names to peer Traffic Server instances. The Service names are URLs which will in the original HTTP request made by the Client after connecting to the ingress Traffic Server. This means the FQDN for the Service is not resolved in the environment of the peer Traffic Server and not the ingress Traffic Server.

Configuration

TLS Bridge requires at least two instances of Traffic Server (Ingress and Peer).

  1. Disable caching on Traffic Server in records.config:

    CONFIG proxy.config.http.cache.http INT 0
    
  2. Configure the ports.

    • The Peer Traffic Server must be listening on an SSL enabled proxy port. For instance, if the proxy port for the Peer is 4443, then configuration in records.config would have:

      CONFIG proxy.config.http.server_ports STRING 4443:ssl
      
    • The Ingress Traffic Server must allow CONNECT to the Peer proxy port. This would be set in records.config by:

      CONFIG proxy.config.http.connect_ports STRING 4443
      

      The Ingress Traffic Server also needs proxy.config.http.server_ports configured to have proxy ports to which the Client can connect.

  3. Remap is not required, however, Traffic Server requires remap in order to accept the request. This can be done by disabling the remap requirement:

    CONFIG proxy.config.url_remap.remap_required INT 0
    

    In this case Traffic Server will act as an open proxy which is unlikely to be a good idea. Traffic Server will need to run in a restricted environment or use access control (via ip_allow.config or iptables).

  4. Configure the Ingress Traffic Server to verify the Peer server certificate:

    CONFIG proxy.config.ssl.client.verify.server INT 1
    
  5. Configure Certificate Authority used by the Ingress Traffic Server to verify the Peer server certificate. If this is a directory all of the certificates in the directory are treated as Certificate Authorites.

    CONFIG proxy.config.ssl.client.CA.cert.filename STRING </path/to/CA_certificate_file_name>
    
  6. Configure the Ingress Traffic Server to provide a client certificate:

    CONFIG proxy.config.ssl.client.cert.path STRING </path/to/certificate/dir>
    CONFIG proxy.config.ssl.client.cert.filename STRING <server_certificate_file_name>
    
  7. Configure the Peer Traffic Server to verify the Ingress client certificate:

    CONFIG proxy.config.ssl.client.certification_level INT 2
    
  8. Enable the TLS Bridge plugin in plugin.config. The plugin is configured by arguments in plugin.config. These are arguments are in pairs of a destination and a peer. The destination is a anchored regular expression which is matched against the host name in the Client CONNECT. The destinations are checked in order and the first match is used to select the Peer Traffic Server. The peer should be an FQDN or IP address with an optional port. For the example above, if the Peer Traffic Server was named “peer.example.com” on port 4443 and the Service at *.service.com, the peer argument would be “peer.example.com:4443”. In plugin.config this would be:

    tls_bridge.so .*[.]service[.]com peer.example.com:4443
    

Notes

TLS Bridge is distinct from more basic Layer 4 Routing available in Traffic Server. For the latter there is no intercept or change of the TLS exchange between the Client and the Service. The exchange looks like this

actor Client
participant "Ingress TS" as Ingress
participant Service

Client <-[#green]> Ingress : //TCP Connect//
Client -[#blue]-> Ingress : <font color="blue">TLS: ""CLIENT HELLO""</font>
note over Ingress : Map SNI to upstream Service
Ingress <-[#green]> Service : //TCP Connect//
Ingress -[#blue]-> Service : <font color="blue">TLS: ""CLIENT HELLO""</font>
note right : Duplicate of data from Client.
note over Ingress : Forward bytes between Client <&arrow-thick-left> <&arrow-thick-right> Service
Client <--> Service

The key points are

  • Traffic Server does no TLS negotiation at all. The properties of the connection between the Ingress Traffic Server and the Service are completely determined by the Client and Server negotation.
  • No packets are modified, the “”CLIENT HELLO”” sent by the Ingress Traffic Server is an exact copy of that sent to the Ingress Traffic Server by the Client. It is only examined for the SNI data in order to select the Service.

Implementation

The TLS Bridge plugin uses TSHttpTxnIntercept to gain control of the ingress Client session. If the session is valid then a separate connection to the peer Traffic Server is created using TSHttpConnect.

After the ingress Traffic Server connects to the peer Traffic Server it sends a duplicate of the Client CONNECT request. This is processed by the peer Traffic Server to connect on to the Service. After this both Traffic Server instances then tunnel data between the Client and the Service, in effect becoming a transparent tunnel.

The overall exchange looks like the following:

@startuml

box "Client Network" #DDFFDD
actor Client
entity "User Agent\nVConn" as lvc
participant "Ingress ATS" as ingress
entity "Upstream\nVConn" as rvc
end box
box "Corporate Network" #DDDDFF
participant "Peer ATS" as peer
database Service
end box

Client -> ingress : TCP or TLS connect
activate lvc
Client -> ingress : HTTP CONNECT
ingress -> lvc : Intercept Transaction
ingress -> peer : TLS connect
activate rvc
note over ingress,peer : Secure Tunnel
ingress -> peer : HTTP CONNECT
note over peer : DNS for Service is\ndone here.
peer -> Service : TCP Connect

note over Client, Service : At this point data can flow between the Client and Server\nover the secure link as a virtual connection, including any TLS handshake.
Client <--> Service
lvc <-> ingress : <&arrow-thick-left> Move data <&arrow-thick-right>
ingress <-> rvc : <&arrow-thick-left> Move data <&arrow-thick-right>
note over ingress : Plugin explicitlys moves this data.

@enduml

A detailed view of the plugin operation.

@startuml

scale max 720 width

ReadRequestHdr : Check for ""CONNECT""
ReadRequestHdr : =====
ReadRequestHdr : Find Peer for Service.

Intercept : Intercept Client Transaction.
Intercept : =====
Intercept : Initialize Bridge Context.

Accept : Initialize ""VConn"" data.
Accept : =====
Accept : Create internal transaction.
Accept : =====
Accept : Set up Client side tunnel.
Accept : =====
Accept : ""CONNECT"" to Peer via internal transaction.

Tunnel : Move data.

state "Flow To Peer" as FlowToPeer
FlowToPeer : Move data from Client ""TSIOBufferReader""\nto Peer ""TSIOBuffer"".
FlowToPeer : =====
FlowToPeer : Reenable VIOs

state "Flow To Client" as FlowToClient
FlowToClient : Move data from Peer ""TSIOBufferReader""\nto Client ""TSIOBuffer"".
FlowToClient : =====
FlowToClient : Reenable VIOs

state "Wait For Peer Response" as WaitForPeerResponse {
  WaitForStatusCode : Parse for status code.

  WaitForResponseEnd : Parse for double newline.

  BadStatus : Set error data\nin Client Response.

  PeerReady : Update Client Response.
  PeerReady : =====
  PeerReady : Set up peer tunnel.
  PeerReady : =====
  PeerReady : Start Tunneling.

  [*] --> WaitForStatusCode
  WaitForStatusCode --> WaitForResponseEnd
  WaitForStatusCode --> BadStatus
  BadStatus --> [*]
  WaitForResponseEnd --> PeerReady
  PeerReady --> [*]
}

[*] --> ReadRequestHdr : ""CONNECT"" Service
ReadRequestHdr --> [*] : Not matched.
ReadRequestHdr --> Intercept
Intercept --> Accept : ""TS_EVENT_NET_ACCEPT""
Accept -r-> WaitForPeerResponse
WaitForPeerResponse --> WaitForPeerResponse : ""TS_EVENT_VCONN_READ_READY""
WaitForPeerResponse --> Tunnel : 200 OK
WaitForPeerResponse -u-> [*] : Peer connect failure

Tunnel --> FlowToClient : ""TS_EVENT_VCONN_READ_READY""\nPeer VIO
FlowToClient --> Tunnel
Tunnel --> FlowToPeer : ""TS_EVENT_VCONN_READY_READY""\nClient VIO
FlowToPeer --> Tunnel

Tunnel -right-> Shutdown : ""TS_EVENT_VCONN_EOS""

Shutdown : Close Client VConn
Shutdown : =====
Shutdown : Close Upstream VConn

@enduml

A sequence diagram focusing on the request / response data flow. There is a NetVConn for the connection to the Peer Traffic Server which is omitted for clarity.

  • Blue dotted lines are request or response data
  • Green lines are network connections.
  • Red lines are programmatic interactions.
  • Black lines are hook call backs.

The 200 OK sent from the Peer Traffic Server is parsed and consumed by the plugin. An non-200 response means there was an error and the tunnel is shut down. To deal with the Client response clean up the response code is stored and used later during cleanup.

@startuml

scale max 720 width

actor Client
box "Ingress ATS" #DDFFDD
entity "Client\nNetVConn" as uanet
participant "Ingress\nATS" as ingress
entity "Client\nVConn" as uavc
entity "TLS Bridge" as plugin
entity "Peer\nVConn" as peervc
end box
box "Peer ATS" #DDDDFF
participant "Peer\nATS" as peer
end box
participant Service

Client <-[#green]> ingress : <font color="green">//TCP//</font> Handshake
activate uanet
Client -[#blue]-> uanet : <font color="blue">""CONNECT"" Service</font>
uanet -> ingress : //Parse request//
ingress -[#black]> plugin : ""READ_REQUEST_HDR_HOOK""
plugin -> ingress : //TSHttpTxnIntercept()//
ingress -> uavc : //Create//
activate uavc
uanet -[#blue]-> ingress : <font color="blue">""CONNECT"" Service</font>
ingress -[#blue]-> uavc : <font color="blue">""CONNECT"" Service</font>
ingress -[#black]> plugin : ""TS_EVENT_NET_ACCEPT""
note right : Client VConn is passed in event data.

plugin -\ ingress : //TSHttpConnect()//
ingress -> peervc : //create//
activate peervc
ingress -/ plugin : //return Peer VConn//

plugin -[#blue]-> peervc : <font color="blue">""CONNECT"" Peer</font>
peervc -> ingress : //parse request//
ingress <-[#green]> peer : <font color="green">//TCP//</font> Handshake
ingress <-[#green]> peer : <font color="green">//TLS//</font> Handshake
ingress -[#blue]-> peervc : <font color="blue">""200 OK""</font>
peervc -[#blue]-> plugin : <font color="blue">""200 OK""</font>
note left
This signals a raw TLS connection
nto the Peer ATS. The response is
parsed and consumed by Plugin.
end note

note over plugin : Plugin switches to byte forwarding.
uavc -[#blue]-> plugin : <font color="blue">""CONNECT"" Service</font>
note left: Original Client Request.
plugin -[#blue]-> peervc : <font color="blue">""CONNECT"" Service</font>
peervc -[#blue]-> peer : <font color="blue">""CONNECT"" Service</font>
peer <-[#green]> Service : <font color="green">//TCP//</font> Handshake
peer <-[#green]> Service : <font color="green">//TLS//</font> Handshake
note left : Optional, based on 'Service'
peer -[#blue]-> peervc : <font color="blue">""200 OK""</font>
peervc -[#blue]-> plugin : <font color="blue">""200 OK""</font>
plugin -[#blue]-> uavc : <font color="blue">""200 OK""</font>
uavc -[#blue]-> ingress : <font color="blue">""200 OK""</font>
note left
ATS updates the incoming response based on
local configuration. This means what goes out
to the Client may be different than what the
plugin wrote (forwarded from Peer ATS).
end note
ingress -[#black]> plugin : ""SEND_RESPONSE_HDR_HOOK""
note right : Plugin cleans up response here.
ingress -[#blue]-> uanet : <font color="blue">""200 OK""</font>
uanet -[#blue]-> Client : <font color="blue">""200 OK""</font>

Client <-[#green]> Service : //TCP/TLS Connect//

@enduml

A restartable state machine is used to recognize the end of the Peer Traffic Server response. The initial part of the response is easy because all that is needed is to wait until there is sufficient data for a minimal parse. The end can be an arbitrary distance in to the stream and may not all be in the same socket read.

@startuml
[*] -r> State_0
State_0 --> State_1 : CR
State_1 --> State_0 : *
State_1 --> State_1 : CR
State_1 --> State_2 : LF
State_2 --> State_3 : CR
State_2 --> State_0 : *
State_3 -r> [*] : LF
State_3 --> State_1 : CR
State_3 --> State_0 : *
@enduml