You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
200 lines
6.6 KiB
200 lines
6.6 KiB
package api
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
|
|
connectip "github.com/Diniboy1123/connect-ip-go"
|
|
"github.com/quic-go/quic-go"
|
|
"github.com/quic-go/quic-go/http3"
|
|
"github.com/yosida95/uritemplate/v3"
|
|
)
|
|
|
|
// fixedPeerConn wraps a net.PacketConn and makes it behave like a point-to-point
|
|
// connection to a fixed peer (e.g. the Cloudflare MASQUE endpoint).
|
|
// This is critical when using a TURN relay as the QUIC transport: the relay
|
|
// conn knows how to send/receive via TURN indications, but quic-go needs
|
|
// the connection to look like a direct pipe to the remote.
|
|
// Matches the fixedPeerConn from the working vk-turn-usque-old implementation.
|
|
type fixedPeerConn struct {
|
|
net.PacketConn
|
|
peer net.Addr
|
|
}
|
|
|
|
func (c *fixedPeerConn) Write(p []byte) (n int, err error) {
|
|
return c.PacketConn.WriteTo(p, c.peer)
|
|
}
|
|
|
|
func (c *fixedPeerConn) Read(p []byte) (n int, err error) {
|
|
n, _, err = c.PacketConn.ReadFrom(p)
|
|
return n, err
|
|
}
|
|
|
|
func (c *fixedPeerConn) RemoteAddr() net.Addr {
|
|
return c.peer
|
|
}
|
|
|
|
// PrepareTlsConfig creates a TLS configuration using the provided certificate and SNI (Server Name Indication).
|
|
// It also verifies the peer's public key against the provided public key.
|
|
//
|
|
// Parameters:
|
|
// - privKey: *ecdsa.PrivateKey - The private key to use for TLS authentication.
|
|
// - peerPubKey: *ecdsa.PublicKey - The endpoint's public key to pin to.
|
|
// - cert: [][]byte - The certificate chain to use for TLS authentication.
|
|
// - sni: string - The Server Name Indication (SNI) to use.
|
|
//
|
|
// Returns:
|
|
// - *tls.Config: A TLS configuration for secure communication.
|
|
// - error: An error if TLS setup fails.
|
|
func PrepareTlsConfig(privKey *ecdsa.PrivateKey, peerPubKey *ecdsa.PublicKey, cert [][]byte, sni string) (*tls.Config, error) {
|
|
tlsConfig := &tls.Config{
|
|
Certificates: []tls.Certificate{
|
|
{
|
|
Certificate: cert,
|
|
PrivateKey: privKey,
|
|
},
|
|
},
|
|
ServerName: sni,
|
|
NextProtos: []string{http3.NextProtoH3},
|
|
MinVersion: tls.VersionTLS12,
|
|
MaxVersion: tls.VersionTLS13,
|
|
CurvePreferences: []tls.CurveID{
|
|
tls.X25519,
|
|
tls.CurveP256,
|
|
},
|
|
CipherSuites: []uint16{
|
|
tls.TLS_AES_128_GCM_SHA256,
|
|
tls.TLS_AES_256_GCM_SHA384,
|
|
tls.TLS_CHACHA20_POLY1305_SHA256,
|
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
|
},
|
|
// WARN: SNI is usually not for the endpoint, so we must skip verification
|
|
InsecureSkipVerify: true,
|
|
// we pin to the endpoint public key
|
|
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
|
if len(rawCerts) == 0 {
|
|
return nil
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(rawCerts[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, ok := cert.PublicKey.(*ecdsa.PublicKey); !ok {
|
|
// we only support ECDSA
|
|
// TODO: don't hardcode cert type in the future
|
|
// as backend can start using different cert types
|
|
return x509.ErrUnsupportedAlgorithm
|
|
}
|
|
|
|
if !cert.PublicKey.(*ecdsa.PublicKey).Equal(peerPubKey) {
|
|
// reason is incorrect, but the best I could figure
|
|
// detail explains the actual reason
|
|
|
|
//10 is NoValidChains, but we support go1.22 where it's not defined
|
|
return x509.CertificateInvalidError{Cert: cert, Reason: 10, Detail: "remote endpoint has a different public key than what we trust in config.json"}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
// ConnectTunnel establishes a QUIC connection and sets up a Connect-IP tunnel with the provided endpoint.
|
|
// Endpoint address is used to check whether the authentication/connection is successful or not.
|
|
// Requires modified connect-ip-go for now to support Cloudflare's non RFC compliant implementation.
|
|
//
|
|
// Parameters:
|
|
// - ctx: context.Context - The QUIC TLS context.
|
|
// - tlsConfig: *tls.Config - The TLS configuration for secure communication.
|
|
// - quicConfig: *quic.Config - The QUIC configuration settings.
|
|
// - connectUri: string - The URI template for the Connect-IP request.
|
|
// - endpoint: *net.UDPAddr - The UDP address of the QUIC server.
|
|
// - baseConn: net.PacketConn - Optional pre-allocated connection (e.g. from VK TURN relay). If nil, a new UDP socket is created.
|
|
//
|
|
// Returns:
|
|
// - net.PacketConn: The packet connection used for the QUIC session.
|
|
// - *http3.Transport: The HTTP/3 transport used for initial request.
|
|
// - *connectip.Conn: The Connect-IP connection instance.
|
|
// - *http.Response: The response from the Connect-IP handshake.
|
|
// - error: An error if the connection setup fails.
|
|
func ConnectTunnel(ctx context.Context, tlsConfig *tls.Config, quicConfig *quic.Config, connectUri string, endpoint *net.UDPAddr, baseConn net.PacketConn) (net.PacketConn, *http3.Transport, *connectip.Conn, *http.Response, error) {
|
|
var conn net.PacketConn
|
|
var err error
|
|
|
|
if baseConn != nil {
|
|
// Wrap the TURN relay conn in fixedPeerConn so quic-go sees it as a
|
|
// point-to-point connection to the Cloudflare endpoint.
|
|
// Without this wrapping, some QUIC packet flows don't survive the
|
|
// TURN relay hop (e.g. keepalives and connect-ip IP packets time out).
|
|
conn = &fixedPeerConn{PacketConn: baseConn, peer: endpoint}
|
|
} else {
|
|
// Create a new UDP socket for direct connection to the Cloudflare MASQUE endpoint
|
|
var udpConn *net.UDPConn
|
|
if endpoint.IP.To4() == nil {
|
|
udpConn, err = net.ListenUDP("udp", &net.UDPAddr{
|
|
IP: net.IPv6zero,
|
|
Port: 0,
|
|
})
|
|
} else {
|
|
udpConn, err = net.ListenUDP("udp", &net.UDPAddr{
|
|
IP: net.IPv4zero,
|
|
Port: 0,
|
|
})
|
|
}
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
conn = udpConn
|
|
}
|
|
|
|
qconn, err := quic.Dial(
|
|
ctx,
|
|
conn,
|
|
endpoint,
|
|
tlsConfig,
|
|
quicConfig,
|
|
)
|
|
if err != nil {
|
|
return conn, nil, nil, nil, err
|
|
}
|
|
|
|
tr := &http3.Transport{
|
|
EnableDatagrams: true,
|
|
AdditionalSettings: map[uint64]uint64{
|
|
// SETTINGS_H3_DATAGRAM (current IETF RFC 9297) - required by Cloudflare
|
|
0x33: 1,
|
|
// SETTINGS_H3_DATAGRAM_00 (deprecated draft, but official client still sends it)
|
|
0x276: 1,
|
|
},
|
|
DisableCompression: true,
|
|
}
|
|
|
|
hconn := tr.NewClientConn(qconn)
|
|
|
|
additionalHeaders := http.Header{
|
|
"User-Agent": []string{""},
|
|
}
|
|
|
|
template := uritemplate.MustNew(connectUri)
|
|
ipConn, rsp, err := connectip.Dial(ctx, hconn, template, "cf-connect-ip", additionalHeaders, true)
|
|
if err != nil {
|
|
if err.Error() == "CRYPTO_ERROR 0x131 (remote): tls: access denied" {
|
|
return conn, nil, nil, nil, errors.New("login failed! Please double-check if your tls key and cert is enrolled in the Cloudflare Access service")
|
|
}
|
|
return conn, nil, nil, nil, fmt.Errorf("failed to dial connect-ip: %v", err)
|
|
}
|
|
|
|
return conn, tr, ipConn, rsp, nil
|
|
}
|
|
|