|
| 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