diff --git a/application/transports/wrapping/format_first/http/client.go b/application/transports/wrapping/format_first/http/client.go new file mode 100644 index 00000000..7e43b9f3 --- /dev/null +++ b/application/transports/wrapping/format_first/http/client.go @@ -0,0 +1,79 @@ +package http + +import ( + "fmt" + + "github.com/refraction-networking/conjure/application/transports" + pb "github.com/refraction-networking/gotapdance/protobuf" + "google.golang.org/protobuf/proto" +) + +// ClientTransport implements the client side transport interface for the Min transport. The +// significant difference is that there is an instance of this structure per client session, where +// the station side Transport struct has one instance to be re-used for all sessions. +type ClientTransport struct { + // Parameters are fields that will be shared with the station in the registration + Parameters *pb.GenericTransportParams + + // // state tracks fields internal to the registrar that survive for the lifetime + // // of the transport session without being shared - i.e. local derived keys. + // state any +} + +// Name returns a string identifier for the Transport for logging +func (*ClientTransport) Name() string { + return "min" +} + +// String returns a string identifier for the Transport for logging (including string formatters) +func (*ClientTransport) String() string { + return "min" +} + +// ID provides an identifier that will be sent to the conjure station during the registration so +// that the station knows what transport to expect connecting to the chosen phantom. +func (*ClientTransport) ID() pb.TransportType { + return pb.TransportType_Min +} + +// GetParams returns a generic protobuf with any parameters from both the registration and the +// transport. +func (t *ClientTransport) GetParams() proto.Message { + return t.Parameters +} + +// SetParams allows the caller to set parameters associated with the transport, returning an +// error if the provided generic message is not compatible. +func (t *ClientTransport) SetParams(p any) error { + params, ok := p.(*pb.GenericTransportParams) + if !ok { + return fmt.Errorf("unable to parse params") + } + t.Parameters = params + + return nil +} + +// GetDstPort returns the destination port that the client should open the phantom connection to +func (t *ClientTransport) GetDstPort(seed []byte, params any) (uint16, error) { + if t.Parameters == nil || !t.Parameters.GetRandomizeDstPort() { + return defaultPort, nil + } + + return transports.PortSelectorRange(portRangeMin, portRangeMax, seed) +} + +// // Connect creates the connection to the phantom address negotiated in the registration phase of +// // Conjure connection establishment. +// func (t *ClientTransport) Connect(ctx context.Context, reg *cj.ConjureReg) (net.Conn, error) { +// // conn, err := reg.getFirstConnection(ctx, reg.TcpDialer, phantoms) +// // if err != nil { +// // return nil, err +// // } + +// // // Send hmac(seed, str) bytes to indicate to station (min transport) +// // connectTag := conjureHMAC(reg.keys.SharedSecret, "MinTrasportHMACString") +// // conn.Write(connectTag) +// // return conn, nil +// return nil, nil +// } diff --git a/application/transports/wrapping/format_first/http/http.go b/application/transports/wrapping/format_first/http/http.go new file mode 100644 index 00000000..58ed01cd --- /dev/null +++ b/application/transports/wrapping/format_first/http/http.go @@ -0,0 +1,158 @@ +package http + +import ( + "bufio" + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/http" + + dd "github.com/refraction-networking/conjure/application/lib" + "github.com/refraction-networking/conjure/application/transports" + pb "github.com/refraction-networking/gotapdance/protobuf" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" +) + +const ( + httpPrefixRegexString = "" + httpPrefixMinLen = 32 + hmacString = "HTTPTransportHMACString" +) + +const ( + // Earliest client library version ID that supports destination port randomization + randomizeDstPortMinVersion uint = 3 + + // port range boundaries for prefix transport when randomizing + portRangeMin = 1024 + portRangeMax = 65535 + minTagLength = 32 + + defaultPort = 80 +) + +// Transport provides a struct implementing the Transport, WrappingTransport, +// PortRandomizingTransport, and FixedPortTransport interfaces. +type Transport struct{} + +// Name returns the human-friendly name of the transport, implementing the +// Transport interface.. +func (Transport) Name() string { return "HTTPTransport" } + +// LogPrefix returns the prefix used when including this transport in logs, +// implementing the Transport interface. +func (Transport) LogPrefix() string { return "HTTP" } + +// GetIdentifier takes in a registration and returns an identifier for it. This +// identifier should be unique for each registration on a given phantom; +// registrations on different phantoms can have the same identifier. +func (Transport) GetIdentifier(d *dd.DecoyRegistration) string { + return string(d.Keys.ConjureHMAC(hmacString)) +} + +// GetProto returns the next layer protocol that the transport uses. Implements +// the Transport interface. +func (Transport) GetProto() pb.IPProto { + return pb.IPProto_Tcp +} + +// ParseParams gives the specific transport an option to parse a generic object +// into parameters provided by the client during registration. +func (Transport) ParseParams(libVersion uint, data *anypb.Any) (any, error) { + if data == nil { + return nil, nil + } + + // For backwards compatibility we create a generic transport params object + // for transports that existed before the transportParams fields existed. + if libVersion < randomizeDstPortMinVersion { + f := false + return &pb.GenericTransportParams{ + RandomizeDstPort: &f, + }, nil + } + + var m = &pb.GenericTransportParams{} + err := anypb.UnmarshalTo(data, m, proto.UnmarshalOptions{}) + return m, err +} + +// GetDstPort Given the library version, a seed, and a generic object +// containing parameters the transport should be able to return the +// destination port that a clients phantom connection will attempt to reach +func (Transport) GetDstPort(libVersion uint, seed []byte, params any) (uint16, error) { + + if libVersion < randomizeDstPortMinVersion { + return 0, transports.ErrTransportNotSupported + } + + if params == nil { + return defaultPort, nil + } + + parameters, ok := params.(*pb.GenericTransportParams) + if !ok { + return 0, fmt.Errorf("bad parameters provided") + } + + if parameters.GetRandomizeDstPort() { + return transports.PortSelectorRange(portRangeMin, portRangeMax, seed) + } + + return defaultPort, nil +} + +// WrapConnection attempts to wrap the given connection in the transport. It +// takes the information gathered so far on the connection in data, attempts to +// identify itself, and if it positively identifies itself wraps the connection +// in the transport, returning a connection that's ready to be used by others. +// +// If the returned error is nil or non-nil and non-{ transports.ErrTryAgain, +// transports.ErrNotTransport }, the caller may no longer use data or conn. +func (t *Transport) WrapConnection(data *bytes.Buffer, c net.Conn, originalDst net.IP, regManager *dd.RegistrationManager) (*dd.DecoyRegistration, net.Conn, error) { + dataLen := data.Len() + + if dataLen == 0 { + return nil, nil, transports.ErrTryAgain + } + + req, err := http.ReadRequest(bufio.NewReader(data)) + if err != nil { + // fmt.Printf("failed to read request\n%s\n", err) + return nil, nil, transports.ErrNotTransport + } + + hmacIDStr := req.Header.Get("X-Ignore") + if hmacIDStr == "" { + return nil, nil, transports.ErrNotTransport + } + hmacID, err := base64.StdEncoding.DecodeString(hmacIDStr) + if err != nil { + return nil, nil, transports.ErrNotTransport + } + + reg, ok := regManager.GetRegistrations(originalDst)[string(hmacID)] + if !ok { + return nil, nil, transports.ErrNotTransport + } + + if req.ContentLength > 0 { + // buf := make([]byte, req.ContentLength) + // _, err := io.ReadFull(req.Body, buf) + // if err != nil { + // // this would be a very strange case to hit + // return nil, nil, transports.ErrNotTransport + // } + buf, err := io.ReadAll(req.Body) + if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { + // this would be a very strange case to hit + return nil, nil, fmt.Errorf("%w: failed to buffer http body: %w", transports.ErrNotTransport, err) + } + return reg, transports.PrependToConn(c, bytes.NewBuffer(buf)), nil + } + return reg, c, nil +} diff --git a/application/transports/wrapping/format_first/http/http_test.go b/application/transports/wrapping/format_first/http/http_test.go new file mode 100644 index 00000000..63fe2f0d --- /dev/null +++ b/application/transports/wrapping/format_first/http/http_test.go @@ -0,0 +1,173 @@ +package http + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "io" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/refraction-networking/conjure/application/transports" + "github.com/refraction-networking/conjure/application/transports/wrapping/internal/tests" + pb "github.com/refraction-networking/gotapdance/protobuf" +) + +func TestSuccessfulWrap(t *testing.T) { + testSubnetPath := os.Getenv("GOPATH") + "/src/github.com/refraction-networking/conjure/application/lib/test/phantom_subnets.toml" + os.Setenv("PHANTOM_SUBNET_LOCATION", testSubnetPath) + + var transport Transport + manager := tests.SetupRegistrationManager(tests.Transport{Index: pb.TransportType_Prefix, Transport: transport}) + c2p, sfp, reg := tests.SetupPhantomConnections(manager, pb.TransportType_Prefix) + defer c2p.Close() + defer sfp.Close() + require.NotNil(t, reg) + + hmacID := reg.Keys.ConjureHMAC(hmacString) + message := []byte(`test message!`) + + req, err := http.NewRequest(http.MethodGet, "/", bytes.NewReader(message)) + require.Nil(t, err) + req.Header.Add("X-Ignore", base64.StdEncoding.EncodeToString(hmacID)) + err = req.Write(c2p) + require.Nil(t, err) + + var buf [4096]byte + var buffer bytes.Buffer + n, _ := sfp.Read(buf[:]) + buffer.Write(buf[:n]) + + _, wrapped, err := transport.WrapConnection(&buffer, sfp, reg.PhantomIp, manager) + require.Nil(t, err, "error getting wrapped connection") + + received := make([]byte, len(message)) + _, err = io.ReadFull(wrapped, received) + require.Nil(t, err, "failed reading from connection") + require.True(t, bytes.Equal(message, received)) +} + +func TestUnsuccessfulWrap(t *testing.T) { + var transport Transport + manager := tests.SetupRegistrationManager(tests.Transport{Index: pb.TransportType_Prefix, Transport: transport}) + c2p, sfp, reg := tests.SetupPhantomConnections(manager, pb.TransportType_Prefix) + defer c2p.Close() + defer sfp.Close() + + message := []byte(`test message!`) + + // No real reason for sending the shared secret; it's just 32 bytes + // (same length as HMAC ID) that should have no significance. + req, err := http.NewRequest(http.MethodGet, "/", bytes.NewReader(message)) + require.Nil(t, err) + req.Header.Add("X-Ignore", base64.StdEncoding.EncodeToString(tests.SharedSecret)) + err = req.Write(c2p) + require.Nil(t, err) + + var buf [128]byte + var buffer bytes.Buffer + n, _ := sfp.Read(buf[:]) + buffer.Write(buf[:n]) + + _, _, err = transport.WrapConnection(&buffer, sfp, reg.PhantomIp, manager) + require.ErrorIs(t, err, transports.ErrNotTransport) +} + +func TestTryAgain(t *testing.T) { + var transport Transport + var err error + manager := tests.SetupRegistrationManager(tests.Transport{Index: pb.TransportType_Prefix, Transport: transport}) + c2p, sfp, reg := tests.SetupPhantomConnections(manager, pb.TransportType_Prefix) + defer c2p.Close() + defer sfp.Close() + + var buffer bytes.Buffer + + // The only way that we should be able to get ErrTryAgain is if it was + // called on a read with 0 bytes + _, _, err = transport.WrapConnection(&buffer, sfp, reg.PhantomIp, manager) + require.ErrorIs(t, err, transports.ErrTryAgain) + message := []byte(`test message!`) + + // No real reason for sending the shared secret; it's just 32 bytes + // (same length as HMAC ID) that should have no significance. + req, err := http.NewRequest(http.MethodGet, "/", bytes.NewReader(message)) + require.Nil(t, err) + req.Header.Add("X-Ignore", base64.StdEncoding.EncodeToString(tests.SharedSecret)) + err = req.Write(c2p) + require.Nil(t, err) + + var buf [128]byte + n, _ := sfp.Read(buf[:]) + buffer.Write(buf[:n]) + + _, _, err = transport.WrapConnection(&buffer, sfp, reg.PhantomIp, manager) + require.ErrorIs(t, err, transports.ErrNotTransport) +} + +func TestSuccessfulWrapLargeMessage(t *testing.T) { + testSubnetPath := os.Getenv("GOPATH") + "/src/github.com/refraction-networking/conjure/application/lib/test/phantom_subnets.toml" + os.Setenv("PHANTOM_SUBNET_LOCATION", testSubnetPath) + + var transport Transport + manager := tests.SetupRegistrationManager(tests.Transport{Index: pb.TransportType_Prefix, Transport: transport}) + c2p, sfp, reg := tests.SetupPhantomConnections(manager, pb.TransportType_Prefix) + defer c2p.Close() + defer sfp.Close() + require.NotNil(t, reg) + + hmacID := reg.Keys.ConjureHMAC(hmacString) + message := make([]byte, 10000) + _, err := rand.Read(message) + require.Nil(t, err) + + req, err := http.NewRequest(http.MethodGet, "/", bytes.NewReader(message)) + require.Nil(t, err) + req.Header.Add("X-Ignore", base64.StdEncoding.EncodeToString(hmacID)) + err = req.Write(c2p) + require.Nil(t, err) + + var buf [4096]byte + var buffer bytes.Buffer + n, _ := sfp.Read(buf[:]) + buffer.Write(buf[:n]) + + _, wrapped, err := transport.WrapConnection(&buffer, sfp, reg.PhantomIp, manager) + require.Nil(t, err, "error getting wrapped connection") + + received := make([]byte, len(message)) + n, err = io.ReadFull(wrapped, received) + require.Nil(t, err, "failed reading from connection") + require.True(t, bytes.Equal(message[:n], received), "xptd: %s\nrecv: %s", hex.EncodeToString(message[:len(received)]), hex.EncodeToString(received)) + // t.Log("l:", n) +} + +func TestTryParamsToDstPort(t *testing.T) { + clv := randomizeDstPortMinVersion + seed, _ := hex.DecodeString("0000000000000000000000000000000000") + + cases := []struct { + r bool + p uint16 + }{{true, 58047}, {false, defaultPort}} + + for _, testCase := range cases { + ct := ClientTransport{Parameters: &pb.GenericTransportParams{RandomizeDstPort: &testCase.r}} + var transport Transport + + rawParams, err := anypb.New(ct.GetParams()) + require.Nil(t, err) + + params, err := transport.ParseParams(clv, rawParams) + require.Nil(t, err) + + port, err := transport.GetDstPort(clv, seed, params) + require.Nil(t, err) + require.Equal(t, testCase.p, port) + } +} diff --git a/application/transports/wrapping/format_first/utls/client.go b/application/transports/wrapping/format_first/utls/client.go new file mode 100644 index 00000000..3f2c16b9 --- /dev/null +++ b/application/transports/wrapping/format_first/utls/client.go @@ -0,0 +1,79 @@ +package utls + +import ( + "fmt" + + "github.com/refraction-networking/conjure/application/transports" + pb "github.com/refraction-networking/gotapdance/protobuf" + "google.golang.org/protobuf/proto" +) + +// ClientTransport implements the client side transport interface for the Min transport. The +// significant difference is that there is an instance of this structure per client session, where +// the station side Transport struct has one instance to be re-used for all sessions. +type ClientTransport struct { + // Parameters are fields that will be shared with the station in the registration + Parameters *pb.GenericTransportParams + + // // state tracks fields internal to the registrar that survive for the lifetime + // // of the transport session without being shared - i.e. local derived keys. + // state any +} + +// Name returns a string identifier for the Transport for logging +func (*ClientTransport) Name() string { + return "min" +} + +// String returns a string identifier for the Transport for logging (including string formatters) +func (*ClientTransport) String() string { + return "min" +} + +// ID provides an identifier that will be sent to the conjure station during the registration so +// that the station knows what transport to expect connecting to the chosen phantom. +func (*ClientTransport) ID() pb.TransportType { + return pb.TransportType_Min +} + +// GetParams returns a generic protobuf with any parameters from both the registration and the +// transport. +func (t *ClientTransport) GetParams() proto.Message { + return t.Parameters +} + +// SetParams allows the caller to set parameters associated with the transport, returning an +// error if the provided generic message is not compatible. +func (t *ClientTransport) SetParams(p any) error { + params, ok := p.(*pb.GenericTransportParams) + if !ok { + return fmt.Errorf("unable to parse params") + } + t.Parameters = params + + return nil +} + +// GetDstPort returns the destination port that the client should open the phantom connection to +func (t *ClientTransport) GetDstPort(seed []byte, params any) (uint16, error) { + if t.Parameters == nil || !t.Parameters.GetRandomizeDstPort() { + return defaultPort, nil + } + + return transports.PortSelectorRange(portRangeMin, portRangeMax, seed) +} + +// // Connect creates the connection to the phantom address negotiated in the registration phase of +// // Conjure connection establishment. +// func (t *ClientTransport) Connect(ctx context.Context, reg *cj.ConjureReg) (net.Conn, error) { +// // conn, err := reg.getFirstConnection(ctx, reg.TcpDialer, phantoms) +// // if err != nil { +// // return nil, err +// // } + +// // // Send hmac(seed, str) bytes to indicate to station (min transport) +// // connectTag := conjureHMAC(reg.keys.SharedSecret, "MinTrasportHMACString") +// // conn.Write(connectTag) +// // return conn, nil +// return nil, nil +// } diff --git a/application/transports/wrapping/format_first/utls/utls.go b/application/transports/wrapping/format_first/utls/utls.go new file mode 100644 index 00000000..b8c35de1 --- /dev/null +++ b/application/transports/wrapping/format_first/utls/utls.go @@ -0,0 +1,142 @@ +package utls + +import ( + "bytes" + "fmt" + "net" + + dd "github.com/refraction-networking/conjure/application/lib" + "github.com/refraction-networking/conjure/application/transports" + pb "github.com/refraction-networking/gotapdance/protobuf" + tls "github.com/refraction-networking/utls" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" +) + +const ( + httpPrefixRegexString = "" + httpPrefixMinLen = 32 + hmacString = "UTLSTransportHMACString" +) + +const ( + // Earliest client library version ID that supports destination port randomization + randomizeDstPortMinVersion uint = 3 + + // port range boundaries for prefix transport when randomizing + portRangeMin = 1024 + portRangeMax = 65535 + minTagLength = 32 + + defaultPort = 443 +) + +// Transport provides a struct implementing the Transport, WrappingTransport, +// PortRandomizingTransport, and FixedPortTransport interfaces. +type Transport struct{} + +// Name returns the human-friendly name of the transport, implementing the +// Transport interface.. +func (Transport) Name() string { return "UTLSTransport" } + +// LogPrefix returns the prefix used when including this transport in logs, +// implementing the Transport interface. +func (Transport) LogPrefix() string { return "UTLS" } + +// GetIdentifier takes in a registration and returns an identifier for it. This +// identifier should be unique for each registration on a given phantom; +// registrations on different phantoms can have the same identifier. +func (Transport) GetIdentifier(d *dd.DecoyRegistration) string { + return string(d.Keys.ConjureHMAC(hmacString)) +} + +// GetProto returns the next layer protocol that the transport uses. Implements +// the Transport interface. +func (Transport) GetProto() pb.IPProto { + return pb.IPProto_Tcp +} + +// ParseParams gives the specific transport an option to parse a generic object +// into parameters provided by the client during registration. +func (Transport) ParseParams(libVersion uint, data *anypb.Any) (any, error) { + if data == nil { + return nil, nil + } + + // For backwards compatibility we create a generic transport params object + // for transports that existed before the transportParams fields existed. + if libVersion < randomizeDstPortMinVersion { + f := false + return &pb.GenericTransportParams{ + RandomizeDstPort: &f, + }, nil + } + + var m = &pb.GenericTransportParams{} + err := anypb.UnmarshalTo(data, m, proto.UnmarshalOptions{}) + return m, err +} + +// GetDstPort Given the library version, a seed, and a generic object +// containing parameters the transport should be able to return the +// destination port that a clients phantom connection will attempt to reach +func (Transport) GetDstPort(libVersion uint, seed []byte, params any) (uint16, error) { + + if libVersion < randomizeDstPortMinVersion { + return 0, transports.ErrTransportNotSupported + } + + if params == nil { + return defaultPort, nil + } + + parameters, ok := params.(*pb.GenericTransportParams) + if !ok { + return 0, fmt.Errorf("bad parameters provided") + } + + if parameters.GetRandomizeDstPort() { + return transports.PortSelectorRange(portRangeMin, portRangeMax, seed) + } + + return defaultPort, nil +} + +// WrapConnection attempts to wrap the given connection in the transport. It +// takes the information gathered so far on the connection in data, attempts to +// identify itself, and if it positively identifies itself wraps the connection +// in the transport, returning a connection that's ready to be used by others. +// +// If the returned error is nil or non-nil and non-{ transports.ErrTryAgain, +// transports.ErrNotTransport }, the caller may no longer use data or conn. +func (t *Transport) WrapConnection(data *bytes.Buffer, c net.Conn, originalDst net.IP, regManager *dd.RegistrationManager) (*dd.DecoyRegistration, net.Conn, error) { + dataLen := data.Len() + + if dataLen == 0 { + return nil, nil, transports.ErrTryAgain + } + + // fmt.Println(hex.EncodeToString(data.Bytes())) + ch := tls.UnmarshalClientHello(data.Bytes()) + if ch == nil { + // fmt.Printf("failed to read request\n%s\n", err) + return nil, nil, fmt.Errorf("%w: failed to unmarshal tls", transports.ErrNotTransport) + } + + hmacID := ch.Random + reg, ok := regManager.GetRegistrations(originalDst)[string(hmacID)] + if !ok { + return nil, nil, transports.ErrNotTransport + } + + return reg, c, nil +} + +// TODO: +// http params for method and path +// utls params for hello id and SNI +// combined formatFirst transport +// formatAll transport +// prefix transport +// +// ClientTransport connect method. diff --git a/application/transports/wrapping/format_first/utls/utls_test.go b/application/transports/wrapping/format_first/utls/utls_test.go new file mode 100644 index 00000000..f7551395 --- /dev/null +++ b/application/transports/wrapping/format_first/utls/utls_test.go @@ -0,0 +1,250 @@ +package utls + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "io" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/anypb" + + cj "github.com/refraction-networking/conjure/application/lib" + "github.com/refraction-networking/conjure/application/transports" + "github.com/refraction-networking/conjure/application/transports/wrapping/internal/tests" + pb "github.com/refraction-networking/gotapdance/protobuf" + tls "github.com/refraction-networking/utls" +) + +func formatClientPacket(reg *cj.DecoyRegistration, params any) ([]byte, error) { + + // TODO: put these in params + helloID := tls.HelloChrome_102 + config := tls.Config{ServerName: "", InsecureSkipVerify: true} + + uTLSConn := tls.UClient(nil, &config, helloID) + hmacID := reg.Keys.ConjureHMAC(hmacString) + + err := uTLSConn.BuildHandshakeState() // Apply our client hello ID + if err != nil { + return nil, err + } + uTLSConn.SetClientRandom(hmacID) + err = uTLSConn.MarshalClientHello() // apply the updated ch random value + if err != nil { + return nil, err + } + + return uTLSConn.HandshakeState.Hello.Marshal() +} + +func DisabledTestMarshalRandom(t *testing.T) { + hmacID := make([]byte, 32) + _, err := rand.Read(hmacID) + require.Nil(t, err) + + helloID := tls.HelloChrome_102 + config := tls.Config{ServerName: "", InsecureSkipVerify: true} + + uTLSConn := tls.UClient(nil, &config, helloID) + + err = uTLSConn.BuildHandshakeState() + require.Nil(t, err) + uTLSConn.SetClientRandom(hmacID) + + err = uTLSConn.BuildHandshakeState() + require.Nil(t, err) + + // t.Log(hex.EncodeToString(hmacID)) + // t.Log(hex.EncodeToString(uTLSConn.HandshakeState.Hello.Random)) + + b, err := uTLSConn.HandshakeState.Hello.Marshal() + require.Nil(t, err) + + ch := tls.UnmarshalClientHello(b) + require.NotNil(t, ch) + // t.Log(hex.EncodeToString(ch.Random)) + require.True(t, bytes.Equal(ch.Random, hmacID)) +} + +func DisabledTestMarshalSNI(t *testing.T) { + hmacID := [32]byte{} + _, err := rand.Read(hmacID[:]) + require.Nil(t, err) + + helloID := tls.HelloChrome_102 + config := tls.Config{ServerName: hex.EncodeToString(hmacID[:]), InsecureSkipVerify: true} + + uTLSConn := tls.UClient(nil, &config, helloID) + + err = uTLSConn.BuildHandshakeState() + require.Nil(t, err) + + b, err := uTLSConn.HandshakeState.Hello.Marshal() + require.Nil(t, err) + + ch := tls.UnmarshalClientHello(b) + require.NotNil(t, ch) + + recv, err := hex.DecodeString(ch.ServerName) + require.Nil(t, err) + require.True(t, bytes.Equal(recv, hmacID[:])) +} + +func TestSuccessfulWrap(t *testing.T) { + testSubnetPath := os.Getenv("GOPATH") + "/src/github.com/refraction-networking/conjure/application/lib/test/phantom_subnets.toml" + os.Setenv("PHANTOM_SUBNET_LOCATION", testSubnetPath) + + var transport Transport + manager := tests.SetupRegistrationManager(tests.Transport{Index: pb.TransportType_Prefix, Transport: transport}) + c2p, sfp, reg := tests.SetupPhantomConnections(manager, pb.TransportType_Prefix) + defer c2p.Close() + defer sfp.Close() + require.NotNil(t, reg) + + message := []byte(`test message!`) + + connectMsg, err := formatClientPacket(reg, nil) + require.Nil(t, err) + _, err = c2p.Write(connectMsg) + require.Nil(t, err) + + var buf [4096]byte + n, _ := sfp.Read(buf[:]) + + _, wrapped, err := transport.WrapConnection(bytes.NewBuffer(buf[:n]), sfp, reg.PhantomIp, manager) + require.Nil(t, err, "error getting wrapped connection") + + _, err = c2p.Write(message) + require.Nil(t, err) + + received := make([]byte, len(message)) + _, err = io.ReadFull(wrapped, received) + require.Nil(t, err, "failed reading from connection") + require.True(t, bytes.Equal(message, received)) +} + +func TestUnsuccessfulWrap(t *testing.T) { + var transport Transport + manager := tests.SetupRegistrationManager(tests.Transport{Index: pb.TransportType_Prefix, Transport: transport}) + c2p, sfp, reg := tests.SetupPhantomConnections(manager, pb.TransportType_Prefix) + defer c2p.Close() + defer sfp.Close() + + message := []byte(`test message!`) + + // No real reason for sending the shared secret; it's just 32 bytes + // (same length as HMAC ID) that should have no significance. + req, err := http.NewRequest(http.MethodGet, "/", bytes.NewReader(message)) + require.Nil(t, err) + req.Header.Add("X-Ignore", base64.StdEncoding.EncodeToString(tests.SharedSecret)) + err = req.Write(c2p) + require.Nil(t, err) + + var buf [128]byte + var buffer bytes.Buffer + n, _ := sfp.Read(buf[:]) + buffer.Write(buf[:n]) + + _, _, err = transport.WrapConnection(&buffer, sfp, reg.PhantomIp, manager) + require.ErrorIs(t, err, transports.ErrNotTransport) +} + +func TestTryAgain(t *testing.T) { + var transport Transport + var err error + manager := tests.SetupRegistrationManager(tests.Transport{Index: pb.TransportType_Prefix, Transport: transport}) + c2p, sfp, reg := tests.SetupPhantomConnections(manager, pb.TransportType_Prefix) + defer c2p.Close() + defer sfp.Close() + + var buffer bytes.Buffer + + // The only way that we should be able to get ErrTryAgain is if it was + // called on a read with 0 bytes + _, _, err = transport.WrapConnection(&buffer, sfp, reg.PhantomIp, manager) + require.ErrorIs(t, err, transports.ErrTryAgain) + message := []byte(`test message!`) + + // No real reason for sending the shared secret; it's just 32 bytes + // (same length as HMAC ID) that should have no significance. + req, err := http.NewRequest(http.MethodGet, "/", bytes.NewReader(message)) + require.Nil(t, err) + req.Header.Add("X-Ignore", base64.StdEncoding.EncodeToString(tests.SharedSecret)) + err = req.Write(c2p) + require.Nil(t, err) + + var buf [128]byte + n, _ := sfp.Read(buf[:]) + buffer.Write(buf[:n]) + + _, _, err = transport.WrapConnection(&buffer, sfp, reg.PhantomIp, manager) + require.ErrorIs(t, err, transports.ErrNotTransport) +} + +func TestSuccessfulWrapLargeMessage(t *testing.T) { + testSubnetPath := os.Getenv("GOPATH") + "/src/github.com/refraction-networking/conjure/application/lib/test/phantom_subnets.toml" + os.Setenv("PHANTOM_SUBNET_LOCATION", testSubnetPath) + + var transport Transport + manager := tests.SetupRegistrationManager(tests.Transport{Index: pb.TransportType_Prefix, Transport: transport}) + c2p, sfp, reg := tests.SetupPhantomConnections(manager, pb.TransportType_Prefix) + defer c2p.Close() + defer sfp.Close() + require.NotNil(t, reg) + + connectMsg, err := formatClientPacket(reg, nil) + require.Nil(t, err) + _, err = c2p.Write(connectMsg) + require.Nil(t, err) + + var buf [4096]byte + var buffer bytes.Buffer + n, _ := sfp.Read(buf[:]) + buffer.Write(buf[:n]) + + _, wrapped, err := transport.WrapConnection(&buffer, sfp, reg.PhantomIp, manager) + require.Nil(t, err, "error getting wrapped connection") + + message := make([]byte, 10000) + _, err = rand.Read(message) + require.Nil(t, err) + + _, err = c2p.Write(message) + require.Nil(t, err) + + received := make([]byte, len(message)) + n, err = io.ReadFull(wrapped, received) + require.Nil(t, err, "failed reading from connection") + require.True(t, bytes.Equal(message[:n], received), "xptd: %s\nrecv: %s", hex.EncodeToString(message[:len(received)]), hex.EncodeToString(received)) + // t.Log("l:", n) +} + +func TestTryParamsToDstPort(t *testing.T) { + clv := randomizeDstPortMinVersion + seed, _ := hex.DecodeString("0000000000000000000000000000000000") + + cases := []struct { + r bool + p uint16 + }{{true, 58047}, {false, defaultPort}} + + for _, testCase := range cases { + ct := ClientTransport{Parameters: &pb.GenericTransportParams{RandomizeDstPort: &testCase.r}} + var transport Transport + + rawParams, err := anypb.New(ct.GetParams()) + require.Nil(t, err) + + params, err := transport.ParseParams(clv, rawParams) + require.Nil(t, err) + + port, err := transport.GetDstPort(clv, seed, params) + require.Nil(t, err) + require.Equal(t, testCase.p, port) + } +} diff --git a/go.mod b/go.mod index 3f5ab1ca..af2356bd 100644 --- a/go.mod +++ b/go.mod @@ -16,26 +16,31 @@ require ( github.com/oschwald/geoip2-golang v1.8.0 github.com/pebbe/zmq4 v1.2.9 github.com/pelletier/go-toml v1.9.5 - github.com/refraction-networking/gotapdance v1.3.5-0.20230118171142-e3cad8ff1431 + github.com/refraction-networking/gotapdance v1.5.0 + github.com/refraction-networking/utls v1.3.2 github.com/sirupsen/logrus v1.9.0 github.com/stretchr/testify v1.8.1 gitlab.com/yawning/obfs4.git v0.0.0-20220904064028-336a71d6e4cf - golang.org/x/crypto v0.5.0 + golang.org/x/crypto v0.9.0 google.golang.org/grpc v1.52.0 - google.golang.org/protobuf v1.28.1 + google.golang.org/protobuf v1.30.0 ) require ( filippo.io/edwards25519 v1.0.0 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/siphash v1.2.3 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/flynn/noise v1.0.0 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/gaukas/godicttls v0.0.3 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/klauspost/compress v1.16.5 // indirect github.com/oschwald/maxminddb-golang v1.10.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb // indirect - golang.org/x/sys v0.4.0 // indirect + github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507 // indirect + gitlab.com/yawning/edwards25519-extra.git v0.0.0-20220726154925-def713fd18e4 // indirect + golang.org/x/sys v0.8.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b322fa48..dfca5821 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,14 @@ git.torproject.org/pluggable-transports/goptlib.git v1.3.0 h1:G+iuRUblCCC2xnO+0a git.torproject.org/pluggable-transports/goptlib.git v1.3.0/go.mod h1:4PBMl1dg7/3vMWSoWb46eGWlrxkUyn/CAJmxhDLAlDs= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -19,11 +25,15 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk= +github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -32,6 +42,10 @@ github.com/hashicorp/golang-lru v0.6.0 h1:uL2shRDx7RTrOrTCUZEGP/wJUFiUI8QT6E7z5o github.com/hashicorp/golang-lru v0.6.0/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/jmwample/obfs4 v0.0.0-20230113193642-07b111e6b208 h1:6nxlCjgsYnjYafKVqvElKbFL+95Kgg5YWT/GuXUNoD8= github.com/jmwample/obfs4 v0.0.0-20230113193642-07b111e6b208/go.mod h1:9GcM8QNU9/wXtEEH2q8bVOnPI7FtIF6VVLzZ1l6Hgf8= +github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= +github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -54,6 +68,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/refraction-networking/gotapdance v1.3.5-0.20230118171142-e3cad8ff1431 h1:IH+TMIXiYCq42/hCVTJx+UunvBrI2QJcw7JNarfdSKg= github.com/refraction-networking/gotapdance v1.3.5-0.20230118171142-e3cad8ff1431/go.mod h1:qcATigC8P78vvDnXC4Sj4x9CbsmkUNSZKaIYevQ8BLA= +github.com/refraction-networking/gotapdance v1.5.0 h1:F28CrhFhyQLAgk2diKxSn7EIp8XfKFPmI6GlSjimb5k= +github.com/refraction-networking/gotapdance v1.5.0/go.mod h1:MrslNp4kScP2RX23nmVf4kegVsKCL5xqToqN1YEWCZc= +github.com/refraction-networking/utls v1.2.0 h1:U5f8wkij2NVinfLuJdFP3gCMwIHs+EzvhxmYdXgiapo= +github.com/refraction-networking/utls v1.2.0/go.mod h1:NPq+cVqzH7D1BeOkmOcb5O/8iVewAsiVt2x1/eO0hgQ= +github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= +github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E= +github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507 h1:ML7ZNtcln5UBo5Wv7RIv9Xg3Pr5VuRCWLFXEwda54Y4= +github.com/sergeyfrolov/bsbuffer v0.0.0-20180903213811-94e85abb8507/go.mod h1:DbI1gxrXI2jRGw7XGEUZQOOMd6PsnKzRrCKabvvMrwM= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -66,10 +88,14 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb h1:qRSZHsODmAP5qDvb3YsO7Qnf3TRiVbGxNG/WYnlM4/o= gitlab.com/yawning/edwards25519-extra.git v0.0.0-20211229043746-2f91fcc9fbdb/go.mod h1:gvdJuZuO/tPZyhEV8K3Hmoxv/DWud5L4qEQxfYjEUTo= +gitlab.com/yawning/edwards25519-extra.git v0.0.0-20220726154925-def713fd18e4 h1:LeXiZggivkDGgmkl7+r+m/2xj3rd+K/30/0obRKayAU= +gitlab.com/yawning/edwards25519-extra.git v0.0.0-20220726154925-def713fd18e4/go.mod h1:gvdJuZuO/tPZyhEV8K3Hmoxv/DWud5L4qEQxfYjEUTo= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -77,6 +103,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= @@ -88,6 +116,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=