Skip to content

Commit bbefc88

Browse files
committed
WIP utls transport using tls session resumption w/ custom CHs
1 parent d4b74ab commit bbefc88

File tree

3 files changed

+549
-0
lines changed

3 files changed

+549
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package utls
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/refraction-networking/conjure/application/transports"
7+
pb "github.com/refraction-networking/gotapdance/protobuf"
8+
"google.golang.org/protobuf/proto"
9+
)
10+
11+
// ClientTransport implements the client side transport interface for the Min transport. The
12+
// significant difference is that there is an instance of this structure per client session, where
13+
// the station side Transport struct has one instance to be re-used for all sessions.
14+
type ClientTransport struct {
15+
// Parameters are fields that will be shared with the station in the registration
16+
Parameters *pb.GenericTransportParams
17+
18+
// // state tracks fields internal to the registrar that survive for the lifetime
19+
// // of the transport session without being shared - i.e. local derived keys.
20+
// state any
21+
}
22+
23+
// Name returns a string identifier for the Transport for logging
24+
func (*ClientTransport) Name() string {
25+
return "min"
26+
}
27+
28+
// String returns a string identifier for the Transport for logging (including string formatters)
29+
func (*ClientTransport) String() string {
30+
return "min"
31+
}
32+
33+
// ID provides an identifier that will be sent to the conjure station during the registration so
34+
// that the station knows what transport to expect connecting to the chosen phantom.
35+
func (*ClientTransport) ID() pb.TransportType {
36+
return pb.TransportType_Min
37+
}
38+
39+
// GetParams returns a generic protobuf with any parameters from both the registration and the
40+
// transport.
41+
func (t *ClientTransport) GetParams() proto.Message {
42+
return t.Parameters
43+
}
44+
45+
// SetParams allows the caller to set parameters associated with the transport, returning an
46+
// error if the provided generic message is not compatible.
47+
func (t *ClientTransport) SetParams(p any) error {
48+
params, ok := p.(*pb.GenericTransportParams)
49+
if !ok {
50+
return fmt.Errorf("unable to parse params")
51+
}
52+
t.Parameters = params
53+
54+
return nil
55+
}
56+
57+
// GetDstPort returns the destination port that the client should open the phantom connection to
58+
func (t *ClientTransport) GetDstPort(seed []byte, params any) (uint16, error) {
59+
if t.Parameters == nil || !t.Parameters.GetRandomizeDstPort() {
60+
return defaultPort, nil
61+
}
62+
63+
return transports.PortSelectorRange(portRangeMin, portRangeMax, seed)
64+
}
65+
66+
// // Connect creates the connection to the phantom address negotiated in the registration phase of
67+
// // Conjure connection establishment.
68+
// func (t *ClientTransport) Connect(ctx context.Context, reg *cj.ConjureReg) (net.Conn, error) {
69+
// // conn, err := reg.getFirstConnection(ctx, reg.TcpDialer, phantoms)
70+
// // if err != nil {
71+
// // return nil, err
72+
// // }
73+
74+
// // // Send hmac(seed, str) bytes to indicate to station (min transport)
75+
// // connectTag := conjureHMAC(reg.keys.SharedSecret, "MinTrasportHMACString")
76+
// // conn.Write(connectTag)
77+
// // return conn, nil
78+
// return nil, nil
79+
// }
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package utls
2+
3+
import (
4+
"bytes"
5+
"crypto/x509"
6+
"encoding/binary"
7+
"encoding/hex"
8+
"fmt"
9+
"math"
10+
"net"
11+
"regexp"
12+
13+
dd "github.com/refraction-networking/conjure/application/lib"
14+
"github.com/refraction-networking/conjure/application/transports"
15+
pb "github.com/refraction-networking/gotapdance/protobuf"
16+
tls "github.com/refraction-networking/utls"
17+
"google.golang.org/protobuf/proto"
18+
"google.golang.org/protobuf/types/known/anypb"
19+
)
20+
21+
const (
22+
httpPrefixRegexString = ""
23+
httpPrefixMinLen = 32
24+
hmacString = "UTLSTransportHMACString"
25+
)
26+
27+
const (
28+
// Earliest client library version ID that supports destination port randomization
29+
randomizeDstPortMinVersion uint = 3
30+
31+
// port range boundaries for prefix transport when randomizing
32+
portRangeMin = 1024
33+
portRangeMax = 65535
34+
minTagLength = 32
35+
36+
defaultPort = 443
37+
38+
tlsCHPrefix = "\x16\x03\x01"
39+
tlsCHHeaderLen = 5
40+
)
41+
42+
// TODO: using a regex is probably not necessary
43+
var tlsHeaderRegex = regexp.MustCompile(`^\x16\x03\x01(.{2})`)
44+
45+
// Transport provides a struct implementing the Transport, WrappingTransport,
46+
// PortRandomizingTransport, and FixedPortTransport interfaces.
47+
type Transport struct{}
48+
49+
// Name returns the human-friendly name of the transport, implementing the
50+
// Transport interface..
51+
func (Transport) Name() string { return "UTLSTransport" }
52+
53+
// LogPrefix returns the prefix used when including this transport in logs,
54+
// implementing the Transport interface.
55+
func (Transport) LogPrefix() string { return "UTLS" }
56+
57+
// GetIdentifier takes in a registration and returns an identifier for it. This
58+
// identifier should be unique for each registration on a given phantom;
59+
// registrations on different phantoms can have the same identifier.
60+
func (Transport) GetIdentifier(d *dd.DecoyRegistration) string {
61+
return string(d.Keys.ConjureHMAC(hmacString))
62+
}
63+
64+
// GetProto returns the next layer protocol that the transport uses. Implements
65+
// the Transport interface.
66+
func (Transport) GetProto() pb.IPProto {
67+
return pb.IPProto_Tcp
68+
}
69+
70+
// ParseParams gives the specific transport an option to parse a generic object
71+
// into parameters provided by the client during registration.
72+
func (Transport) ParseParams(libVersion uint, data *anypb.Any) (any, error) {
73+
if data == nil {
74+
return nil, nil
75+
}
76+
77+
// For backwards compatibility we create a generic transport params object
78+
// for transports that existed before the transportParams fields existed.
79+
if libVersion < randomizeDstPortMinVersion {
80+
f := false
81+
return &pb.GenericTransportParams{
82+
RandomizeDstPort: &f,
83+
}, nil
84+
}
85+
86+
var m = &pb.GenericTransportParams{}
87+
err := anypb.UnmarshalTo(data, m, proto.UnmarshalOptions{})
88+
return m, err
89+
}
90+
91+
// GetDstPort Given the library version, a seed, and a generic object
92+
// containing parameters the transport should be able to return the
93+
// destination port that a clients phantom connection will attempt to reach
94+
func (Transport) GetDstPort(libVersion uint, seed []byte, params any) (uint16, error) {
95+
96+
if libVersion < randomizeDstPortMinVersion {
97+
return 0, transports.ErrTransportNotSupported
98+
}
99+
100+
if params == nil {
101+
return defaultPort, nil
102+
}
103+
104+
parameters, ok := params.(*pb.GenericTransportParams)
105+
if !ok {
106+
return 0, fmt.Errorf("bad parameters provided")
107+
}
108+
109+
if parameters.GetRandomizeDstPort() {
110+
return transports.PortSelectorRange(portRangeMin, portRangeMax, seed)
111+
}
112+
113+
return defaultPort, nil
114+
}
115+
116+
// WrapConnection attempts to wrap the given connection in the transport. It
117+
// takes the information gathered so far on the connection in data, attempts to
118+
// identify itself, and if it positively identifies itself wraps the connection
119+
// in the transport, returning a connection that's ready to be used by others.
120+
//
121+
// If the returned error is nil or non-nil and non-{ transports.ErrTryAgain,
122+
// transports.ErrNotTransport }, the caller may no longer use data or conn.
123+
func (t *Transport) WrapConnection(data *bytes.Buffer, c net.Conn, originalDst net.IP, regManager *dd.RegistrationManager) (*dd.DecoyRegistration, net.Conn, error) {
124+
dataLen := data.Len()
125+
126+
if dataLen == 0 {
127+
return nil, nil, transports.ErrTryAgain
128+
} else if dataLen < tlsCHHeaderLen {
129+
// If we don't have enough bytes to check for the clientHello, check if we can rule it out
130+
// based on the fixed tls header bytes we expect.
131+
n := int(math.Min(float64(dataLen), float64(len(tlsCHPrefix))))
132+
if !bytes.Equal(data.Bytes()[:n], []byte(tlsCHPrefix)[:n]) {
133+
return nil, nil, transports.ErrNotTransport
134+
}
135+
return nil, nil, transports.ErrTryAgain
136+
}
137+
138+
// 160301{len:2}{clientHello:len}
139+
out := tlsHeaderRegex.FindSubmatch(data.Bytes())
140+
if len(out) < 2 {
141+
return nil, nil, transports.ErrNotTransport
142+
}
143+
144+
// First match is the whole pattern, the second should be the group, and based on the regex it
145+
// should always be the two bytes that we can parse into a u16 for ClientHello length.
146+
chLen := binary.BigEndian.Uint16(out[1])
147+
if dataLen < tlsCHHeaderLen+int(chLen) {
148+
return nil, nil, transports.ErrTryAgain
149+
}
150+
151+
ch := tls.UnmarshalClientHello(data.Bytes()[tlsCHHeaderLen:dataLen])
152+
if ch == nil {
153+
// We assume that one MTU is enough for the clientHello. If we have read the declared
154+
// ClientHello length OR more than 1 MTU and still haven't found our registration then we
155+
// probably wont find it.
156+
if dataLen >= tlsCHHeaderLen+int(chLen) || dataLen > 1500 {
157+
return nil, nil, fmt.Errorf("%w: failed to unmarshal tls", transports.ErrNotTransport)
158+
// fmt.Printf("failed to read request\n%s\n", err)
159+
}
160+
return nil, nil, transports.ErrTryAgain
161+
}
162+
163+
hmacID := ch.Random
164+
sessionID := ch.SessionId
165+
reg, ok := regManager.GetRegistrations(originalDst)[string(xorBytes(sessionID, hmacID))]
166+
if !ok {
167+
return nil, nil, transports.ErrNotTransport
168+
}
169+
170+
config := &tls.Config{
171+
Certificates: make([]tls.Certificate, 2),
172+
InsecureSkipVerify: true,
173+
MinVersion: tls.VersionTLS10,
174+
MaxVersion: tls.VersionTLS13,
175+
}
176+
config.Certificates[0].Certificate = [][]byte{testRSACertificate}
177+
config.Certificates[0].PrivateKey = testRSAPrivateKey
178+
config.Certificates[1].Certificate = [][]byte{testSNICertificate}
179+
config.Certificates[1].PrivateKey = testRSAPrivateKey
180+
config.BuildNameToCertificate()
181+
182+
tlsConn := tls.Server(transports.PrependToConn(c, data), config)
183+
return reg, tlsConn, nil
184+
}
185+
186+
func xorBytes(a, b []byte) []byte {
187+
if len(a) != len(b) {
188+
return []byte{}
189+
}
190+
191+
n := make([]byte, len(a))
192+
for i := 0; i < len(a); i++ {
193+
n[i] = a[i] ^ b[i]
194+
}
195+
196+
return n
197+
}
198+
199+
func fromHex(s string) []byte {
200+
b, _ := hex.DecodeString(s)
201+
return b
202+
}
203+
204+
var testRSAPrivateKey, _ = x509.ParsePKCS1PrivateKey(fromHex("3082025b02010002818100db467d932e12270648bc062821ab7ec4b6a25dfe1e5245887a3647a5080d92425bc281c0be97799840fb4f6d14fd2b138bc2a52e67d8d4099ed62238b74a0b74732bc234f1d193e596d9747bf3589f6c613cc0b041d4d92b2b2423775b1c3bbd755dce2054cfa163871d1e24c4f31d1a508baab61443ed97a77562f414c852d702030100010281800b07fbcf48b50f1388db34b016298b8217f2092a7c9a04f77db6775a3d1279b62ee9951f7e371e9de33f015aea80660760b3951dc589a9f925ed7de13e8f520e1ccbc7498ce78e7fab6d59582c2386cc07ed688212a576ff37833bd5943483b5554d15a0b9b4010ed9bf09f207e7e9805f649240ed6c1256ed75ab7cd56d9671024100fded810da442775f5923debae4ac758390a032a16598d62f059bb2e781a9c2f41bfa015c209f966513fe3bf5a58717cbdb385100de914f88d649b7d15309fa49024100dd10978c623463a1802c52f012cfa72ff5d901f25a2292446552c2568b1840e49a312e127217c2186615aae4fb6602a4f6ebf3f3d160f3b3ad04c592f65ae41f02400c69062ca781841a09de41ed7a6d9f54adc5d693a2c6847949d9e1358555c9ac6a8d9e71653ac77beb2d3abaf7bb1183aa14278956575dbebf525d0482fd72d90240560fe1900ba36dae3022115fd952f2399fb28e2975a1c3e3d0b679660bdcb356cc189d611cfdd6d87cd5aea45aa30a2082e8b51e94c2f3dd5d5c6036a8a615ed0240143993d80ece56f877cb80048335701eb0e608cc0c1ca8c2227b52edf8f1ac99c562f2541b5ce81f0515af1c5b4770dba53383964b4b725ff46fdec3d08907df"))
205+
var testRSACertificate = fromHex("3082024b308201b4a003020102020900e8f09d3fe25beaa6300d06092a864886f70d01010b0500301f310b3009060355040a1302476f3110300e06035504031307476f20526f6f74301e170d3136303130313030303030305a170d3235303130313030303030305a301a310b3009060355040a1302476f310b300906035504031302476f30819f300d06092a864886f70d010101050003818d0030818902818100db467d932e12270648bc062821ab7ec4b6a25dfe1e5245887a3647a5080d92425bc281c0be97799840fb4f6d14fd2b138bc2a52e67d8d4099ed62238b74a0b74732bc234f1d193e596d9747bf3589f6c613cc0b041d4d92b2b2423775b1c3bbd755dce2054cfa163871d1e24c4f31d1a508baab61443ed97a77562f414c852d70203010001a38193308190300e0603551d0f0101ff0404030205a0301d0603551d250416301406082b0601050507030106082b06010505070302300c0603551d130101ff0402300030190603551d0e041204109f91161f43433e49a6de6db680d79f60301b0603551d230414301280104813494d137e1631bba301d5acab6e7b30190603551d1104123010820e6578616d706c652e676f6c616e67300d06092a864886f70d01010b0500038181009d30cc402b5b50a061cbbae55358e1ed8328a9581aa938a495a1ac315a1a84663d43d32dd90bf297dfd320643892243a00bccf9c7db74020015faad3166109a276fd13c3cce10c5ceeb18782f16c04ed73bbb343778d0c1cf10fa1d8408361c94c722b9daedb4606064df4c1b33ec0d1bd42d4dbfe3d1360845c21d33be9fae7")
206+
var testSNICertificate = fromHex("0441883421114c81480804c430820237308201a0a003020102020900e8f09d3fe25beaa6300d06092a864886f70d01010b0500301f310b3009060355040a1302476f3110300e06035504031307476f20526f6f74301e170d3136303130313030303030305a170d3235303130313030303030305a3023310b3009060355040a1302476f311430120603550403130b736e69746573742e636f6d30819f300d06092a864886f70d010101050003818d0030818902818100db467d932e12270648bc062821ab7ec4b6a25dfe1e5245887a3647a5080d92425bc281c0be97799840fb4f6d14fd2b138bc2a52e67d8d4099ed62238b74a0b74732bc234f1d193e596d9747bf3589f6c613cc0b041d4d92b2b2423775b1c3bbd755dce2054cfa163871d1e24c4f31d1a508baab61443ed97a77562f414c852d70203010001a3773075300e0603551d0f0101ff0404030205a0301d0603551d250416301406082b0601050507030106082b06010505070302300c0603551d130101ff0402300030190603551d0e041204109f91161f43433e49a6de6db680d79f60301b0603551d230414301280104813494d137e1631bba301d5acab6e7b300d06092a864886f70d01010b0500038181007beeecff0230dbb2e7a334af65430b7116e09f327c3bbf918107fc9c66cb497493207ae9b4dbb045cb63d605ec1b5dd485bb69124d68fa298dc776699b47632fd6d73cab57042acb26f083c4087459bc5a3bb3ca4d878d7fe31016b7bc9a627438666566e3389bfaeebe6becc9a0093ceed18d0f9ac79d56f3a73f18188988ed")
207+
208+
// TODO:
209+
//
210+
// [X] Fill PSK in client and session cache in station to ensure resumption of legit sessions.
211+
//
212+
// [ ] for TLS 1.3 Use session ticket and XOR rand and sessionID so that neither is (apparently) re-used
213+
// [ ] for TLS 1.2 Use session ticket and XOR rand with session ID.
214+
//
215+
// [ ] utls params for hello id and SNI
216+
//
217+
// [ ] Can we leave certs / private keys out if using psk / session resumption?
218+
// [ ] Alternatively, can we generate the private key dynamically after finding the registration?
219+
//
220+
// If we use one key does that allow clients to connect to other clients coverts? probably.

0 commit comments

Comments
 (0)