13 changed files with 615 additions and 22 deletions
@ -0,0 +1,299 @@
|
||||
package reality |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"crypto/ecdh" |
||||
"crypto/ed25519" |
||||
"crypto/hmac" |
||||
"crypto/sha256" |
||||
"crypto/sha512" |
||||
gotls "crypto/tls" |
||||
"crypto/x509" |
||||
"encoding/binary" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"reflect" |
||||
"regexp" |
||||
"strings" |
||||
"sync" |
||||
"time" |
||||
"unsafe" |
||||
|
||||
"github.com/cloudflare/circl/sign/mldsa/mldsa65" |
||||
utls "github.com/refraction-networking/utls" |
||||
"github.com/xtls/reality" |
||||
"github.com/xtls/xray-core/common/crypto" |
||||
"github.com/xtls/xray-core/common/errors" |
||||
"github.com/xtls/xray-core/common/net" |
||||
"github.com/xtls/xray-core/common/utils" |
||||
"github.com/xtls/xray-core/core" |
||||
"github.com/xtls/xray-core/transport/internet/tls" |
||||
"golang.org/x/crypto/hkdf" |
||||
"golang.org/x/net/http2" |
||||
) |
||||
|
||||
type Conn struct { |
||||
*reality.Conn |
||||
} |
||||
|
||||
func (c *Conn) HandshakeAddress() net.Address { |
||||
if err := c.Handshake(); err != nil { |
||||
return nil |
||||
} |
||||
state := c.ConnectionState() |
||||
if state.ServerName == "" { |
||||
return nil |
||||
} |
||||
return net.ParseAddress(state.ServerName) |
||||
} |
||||
|
||||
func Server(c net.Conn, config *reality.Config) (net.Conn, error) { |
||||
realityConn, err := reality.Server(context.Background(), c, config) |
||||
return &Conn{Conn: realityConn}, err |
||||
} |
||||
|
||||
type UConn struct { |
||||
*utls.UConn |
||||
Config *Config |
||||
ServerName string |
||||
AuthKey []byte |
||||
Verified bool |
||||
} |
||||
|
||||
func (c *UConn) HandshakeAddress() net.Address { |
||||
if err := c.Handshake(); err != nil { |
||||
return nil |
||||
} |
||||
state := c.ConnectionState() |
||||
if state.ServerName == "" { |
||||
return nil |
||||
} |
||||
return net.ParseAddress(state.ServerName) |
||||
} |
||||
|
||||
func (c *UConn) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { |
||||
if c.Config.Show { |
||||
localAddr := c.LocalAddr().String() |
||||
fmt.Printf("REALITY localAddr: %v\tis using X25519MLKEM768 for TLS' communication: %v\n", localAddr, c.HandshakeState.ServerHello.ServerShare.Group == utls.X25519MLKEM768) |
||||
fmt.Printf("REALITY localAddr: %v\tis using ML-DSA-65 for cert's extra verification: %v\n", localAddr, len(c.Config.Mldsa65Verify) > 0) |
||||
} |
||||
p, _ := reflect.TypeOf(c.Conn).Elem().FieldByName("peerCertificates") |
||||
certs := *(*([]*x509.Certificate))(unsafe.Pointer(uintptr(unsafe.Pointer(c.Conn)) + p.Offset)) |
||||
if pub, ok := certs[0].PublicKey.(ed25519.PublicKey); ok { |
||||
h := hmac.New(sha512.New, c.AuthKey) |
||||
h.Write(pub) |
||||
if bytes.Equal(h.Sum(nil), certs[0].Signature) { |
||||
if len(c.Config.Mldsa65Verify) > 0 { |
||||
if len(certs[0].Extensions) > 0 { |
||||
h.Write(c.HandshakeState.Hello.Raw) |
||||
h.Write(c.HandshakeState.ServerHello.Raw) |
||||
verify, _ := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.Config.Mldsa65Verify) |
||||
if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) { |
||||
c.Verified = true |
||||
return nil |
||||
} |
||||
} |
||||
} else { |
||||
c.Verified = true |
||||
return nil |
||||
} |
||||
} |
||||
} |
||||
opts := x509.VerifyOptions{ |
||||
DNSName: c.ServerName, |
||||
Intermediates: x509.NewCertPool(), |
||||
} |
||||
for _, cert := range certs[1:] { |
||||
opts.Intermediates.AddCert(cert) |
||||
} |
||||
if _, err := certs[0].Verify(opts); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func UClient(c net.Conn, config *Config, ctx context.Context, dest net.Destination) (net.Conn, error) { |
||||
localAddr := c.LocalAddr().String() |
||||
uConn := &UConn{ |
||||
Config: config, |
||||
} |
||||
utlsConfig := &utls.Config{ |
||||
VerifyPeerCertificate: uConn.VerifyPeerCertificate, |
||||
ServerName: config.ServerName, |
||||
InsecureSkipVerify: true, |
||||
SessionTicketsDisabled: true, |
||||
KeyLogWriter: KeyLogWriterFromConfig(config), |
||||
} |
||||
if utlsConfig.ServerName == "" { |
||||
utlsConfig.ServerName = dest.Address.String() |
||||
} |
||||
uConn.ServerName = utlsConfig.ServerName |
||||
fingerprint := tls.GetFingerprint(config.Fingerprint) |
||||
if fingerprint == nil { |
||||
return nil, errors.New("REALITY: failed to get fingerprint").AtError() |
||||
} |
||||
uConn.UConn = utls.UClient(c, utlsConfig, *fingerprint) |
||||
{ |
||||
uConn.BuildHandshakeState() |
||||
hello := uConn.HandshakeState.Hello |
||||
hello.SessionId = make([]byte, 32) |
||||
copy(hello.Raw[39:], hello.SessionId) // the fixed location of `Session ID`
|
||||
hello.SessionId[0] = core.Version_x |
||||
hello.SessionId[1] = core.Version_y |
||||
hello.SessionId[2] = core.Version_z |
||||
hello.SessionId[3] = 0 // reserved
|
||||
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix())) |
||||
copy(hello.SessionId[8:], config.ShortId) |
||||
if config.Show { |
||||
fmt.Printf("REALITY localAddr: %v\thello.SessionId[:16]: %v\n", localAddr, hello.SessionId[:16]) |
||||
} |
||||
publicKey, err := ecdh.X25519().NewPublicKey(config.PublicKey) |
||||
if err != nil { |
||||
return nil, errors.New("REALITY: publicKey == nil") |
||||
} |
||||
ecdhe := uConn.HandshakeState.State13.KeyShareKeys.Ecdhe |
||||
if ecdhe == nil { |
||||
ecdhe = uConn.HandshakeState.State13.KeyShareKeys.MlkemEcdhe |
||||
} |
||||
if ecdhe == nil { |
||||
return nil, errors.New("Current fingerprint ", uConn.ClientHelloID.Client, uConn.ClientHelloID.Version, " does not support TLS 1.3, REALITY handshake cannot establish.") |
||||
} |
||||
uConn.AuthKey, _ = ecdhe.ECDH(publicKey) |
||||
if uConn.AuthKey == nil { |
||||
return nil, errors.New("REALITY: SharedKey == nil") |
||||
} |
||||
if _, err := hkdf.New(sha256.New, uConn.AuthKey, hello.Random[:20], []byte("REALITY")).Read(uConn.AuthKey); err != nil { |
||||
return nil, err |
||||
} |
||||
aead := crypto.NewAesGcm(uConn.AuthKey) |
||||
if config.Show { |
||||
fmt.Printf("REALITY localAddr: %v\tuConn.AuthKey[:16]: %v\tAEAD: %T\n", localAddr, uConn.AuthKey[:16], aead) |
||||
} |
||||
aead.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) |
||||
copy(hello.Raw[39:], hello.SessionId) |
||||
} |
||||
if err := uConn.HandshakeContext(ctx); err != nil { |
||||
return nil, err |
||||
} |
||||
if config.Show { |
||||
fmt.Printf("REALITY localAddr: %v\tuConn.Verified: %v\n", localAddr, uConn.Verified) |
||||
} |
||||
if !uConn.Verified { |
||||
errors.LogError(ctx, "REALITY: received real certificate (potential MITM or redirection)") |
||||
go func() { |
||||
client := &http.Client{ |
||||
Transport: &http2.Transport{ |
||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *gotls.Config) (net.Conn, error) { |
||||
if config.Show { |
||||
fmt.Printf("REALITY localAddr: %v\tDialTLSContext\n", localAddr) |
||||
} |
||||
return uConn, nil |
||||
}, |
||||
}, |
||||
} |
||||
prefix := []byte("https://" + uConn.ServerName) |
||||
maps.Lock() |
||||
if maps.maps == nil { |
||||
maps.maps = make(map[string]map[string]struct{}) |
||||
} |
||||
paths := maps.maps[uConn.ServerName] |
||||
if paths == nil { |
||||
paths = make(map[string]struct{}) |
||||
paths[config.SpiderX] = struct{}{} |
||||
maps.maps[uConn.ServerName] = paths |
||||
} |
||||
firstURL := string(prefix) + getPathLocked(paths) |
||||
maps.Unlock() |
||||
get := func(first bool) { |
||||
var ( |
||||
req *http.Request |
||||
resp *http.Response |
||||
err error |
||||
body []byte |
||||
) |
||||
if first { |
||||
req, _ = http.NewRequest("GET", firstURL, nil) |
||||
} else { |
||||
maps.Lock() |
||||
req, _ = http.NewRequest("GET", string(prefix)+getPathLocked(paths), nil) |
||||
maps.Unlock() |
||||
} |
||||
if req == nil { |
||||
return |
||||
} |
||||
utils.TryDefaultHeadersWith(req.Header, "nav") |
||||
if first && config.Show { |
||||
fmt.Printf("REALITY localAddr: %v\treq.UserAgent(): %v\n", localAddr, req.UserAgent()) |
||||
} |
||||
times := 1 |
||||
if !first { |
||||
times = int(crypto.RandBetween(config.SpiderY[4], config.SpiderY[5])) |
||||
} |
||||
for j := 0; j < times; j++ { |
||||
if !first && j == 0 { |
||||
req.Header.Set("Referer", firstURL) |
||||
} |
||||
req.AddCookie(&http.Cookie{Name: "padding", Value: strings.Repeat("0", int(crypto.RandBetween(config.SpiderY[0], config.SpiderY[1])))}) |
||||
if resp, err = client.Do(req); err != nil { |
||||
break |
||||
} |
||||
defer resp.Body.Close() |
||||
req.Header.Set("Referer", req.URL.String()) |
||||
if body, err = io.ReadAll(resp.Body); err != nil { |
||||
break |
||||
} |
||||
maps.Lock() |
||||
for _, m := range href.FindAllSubmatch(body, -1) { |
||||
m[1] = bytes.TrimPrefix(m[1], prefix) |
||||
if !bytes.Contains(m[1], dot) { |
||||
paths[string(m[1])] = struct{}{} |
||||
} |
||||
} |
||||
req.URL.Path = getPathLocked(paths) |
||||
if config.Show { |
||||
fmt.Printf("REALITY localAddr: %v\treq.Referer(): %v\n", localAddr, req.Referer()) |
||||
fmt.Printf("REALITY localAddr: %v\tlen(body): %v\n", localAddr, len(body)) |
||||
fmt.Printf("REALITY localAddr: %v\tlen(paths): %v\n", localAddr, len(paths)) |
||||
} |
||||
maps.Unlock() |
||||
if !first { |
||||
time.Sleep(time.Duration(crypto.RandBetween(config.SpiderY[6], config.SpiderY[7])) * time.Millisecond) // interval
|
||||
} |
||||
} |
||||
} |
||||
get(true) |
||||
concurrency := int(crypto.RandBetween(config.SpiderY[2], config.SpiderY[3])) |
||||
for i := 0; i < concurrency; i++ { |
||||
go get(false) |
||||
} |
||||
// Do not close the connection
|
||||
}() |
||||
time.Sleep(time.Duration(crypto.RandBetween(config.SpiderY[8], config.SpiderY[9])) * time.Millisecond) // return
|
||||
return nil, errors.New("REALITY: processed invalid connection").AtWarning() |
||||
} |
||||
return uConn, nil |
||||
} |
||||
|
||||
var ( |
||||
href = regexp.MustCompile(`href="([/h].*?)"`) |
||||
dot = []byte(".") |
||||
) |
||||
|
||||
var maps struct { |
||||
sync.Mutex |
||||
maps map[string]map[string]struct{} |
||||
} |
||||
|
||||
func getPathLocked(paths map[string]struct{}) string { |
||||
stopAt := int(crypto.RandBetween(0, int64(len(paths)-1))) |
||||
i := 0 |
||||
for s := range paths { |
||||
if i == stopAt { |
||||
return s |
||||
} |
||||
i++ |
||||
} |
||||
return "/" |
||||
} |
||||
@ -0,0 +1,94 @@
|
||||
// test_reality_bgp.c — тест reality-сокетов в BGP (topo_node)
|
||||
//
|
||||
// Проверяет:
|
||||
// 1. topo_node_find_reality_sock — поиск по socket_id.
|
||||
// 2. topo_node_apply_reality — заполнение reality_client_config линка.
|
||||
// 3. Serialize → deserialize round-trip reality-сокетов.
|
||||
#include "reality.h" |
||||
#include "topo_node.h" |
||||
#include "topo_group.h" |
||||
#include "etcp_connections.h" |
||||
#include "../lib/debug_config.h" |
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
|
||||
static int test_failed = 0; |
||||
|
||||
#define CHECK(expr, msg) do { \ |
||||
if (!(expr)) { \
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "FAIL: %s", msg); \
|
||||
test_failed = 1; \
|
||||
} else { \
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "PASS: %s", msg); \
|
||||
} \
|
||||
} while (0) |
||||
|
||||
int main(void) { |
||||
debug_config_init(); |
||||
debug_set_level(DEBUG_LEVEL_INFO); |
||||
debug_set_categories(DEBUG_CATEGORY_BGP); |
||||
|
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== Reality BGP Test ==="); |
||||
|
||||
// 1. узел с двумя reality-сокетами
|
||||
struct TOPO_NODE ni; memset(&ni, 0, sizeof(ni)); |
||||
struct TOPO_REALITY_SOCK r1, r2; |
||||
memset(&r1, 0, sizeof(r1)); memset(&r2, 0, sizeof(r2)); |
||||
r1.socket_id = 3; r2.socket_id = 7; |
||||
r1.short_id[0] = 0xAA; r2.short_id[0] = 0xBB; |
||||
r1.version[0] = 1; r1.version[1] = 2; r1.version[2] = 3; |
||||
for (int i = 0; i < 32; i++) r1.server_pubkey[i] = (uint8_t)(i + 1); |
||||
strcpy(r1.server_name, "www.example.com"); |
||||
strcpy(r2.server_name, "api.example.com"); |
||||
r1.next = &r2; r2.next = NULL; |
||||
ni.reality_socks = &r1; |
||||
|
||||
// 2. find
|
||||
CHECK(topo_node_find_reality_sock(&ni, 3) == &r1, "find sock 3"); |
||||
CHECK(topo_node_find_reality_sock(&ni, 7) == &r2, "find sock 7"); |
||||
CHECK(topo_node_find_reality_sock(&ni, 99) == NULL, "find missing socket"); |
||||
CHECK(topo_node_find_reality_sock(NULL, 3) == NULL, "find on NULL node"); |
||||
|
||||
// 3. apply
|
||||
struct ETCP_LINK link; memset(&link, 0, sizeof(link)); |
||||
topo_node_apply_reality(&link, &r1); |
||||
CHECK(link.reality_set == 1, "apply sets reality_set"); |
||||
CHECK(memcmp(link.reality.short_id, r1.short_id, 8) == 0, "apply short_id"); |
||||
CHECK(memcmp(link.reality.server_static_pubkey, r1.server_pubkey, 32) == 0, "apply pubkey"); |
||||
CHECK(memcmp(link.reality.version, r1.version, 3) == 0, "apply version"); |
||||
CHECK(strcmp(link.reality.server_name, "www.example.com") == 0, "apply server_name"); |
||||
topo_node_apply_reality(&link, NULL); |
||||
CHECK(link.reality_set == 1, "apply NULL rs is no-op"); |
||||
|
||||
// 4. serialize → deserialize round-trip
|
||||
struct TOPO_GROUP_NODE nq; memset(&nq, 0, sizeof(nq)); |
||||
uint8_t buf[2048]; |
||||
int len = topo_node_serialize(&ni, &nq, 0x1234, 0, buf, sizeof(buf), 0); |
||||
CHECK(len > 0, "serialize ok"); |
||||
struct TOPO_GROUP g; memset(&g, 0, sizeof(g)); |
||||
struct TOPO_NODE* out_ni = NULL; |
||||
struct TOPO_NODESUBNETS* out_sub = NULL; |
||||
uint64_t* hop = NULL; uint8_t hopc = 0; uint16_t rtt = 0; |
||||
int rc = topo_node_deserialize(&g, buf, len, &out_ni, &out_sub, &hop, &hopc, &rtt); |
||||
CHECK(rc == 0 && out_ni != NULL, "deserialize ok"); |
||||
if (out_ni) { |
||||
int cnt = 0; for (const struct TOPO_REALITY_SOCK* r = out_ni->reality_socks; r; r = r->next) cnt++; |
||||
CHECK(cnt == 2, "round-trip: 2 reality socks"); |
||||
const struct TOPO_REALITY_SOCK* rr = topo_node_find_reality_sock(out_ni, 7); |
||||
CHECK(rr != NULL, "round-trip: find sock 7"); |
||||
if (rr) { |
||||
CHECK(rr->short_id[0] == 0xBB, "round-trip: short_id"); |
||||
CHECK(strcmp(rr->server_name, "api.example.com") == 0, "round-trip: server_name"); |
||||
} |
||||
const struct TOPO_REALITY_SOCK* rr1 = topo_node_find_reality_sock(out_ni, 3); |
||||
CHECK(rr1 != NULL && memcmp(rr1->server_pubkey, r1.server_pubkey, 32) == 0, "round-trip: pubkey"); |
||||
CHECK(rr1 != NULL && rr1->version[0] == 1 && rr1->version[1] == 2 && rr1->version[2] == 3, "round-trip: version"); |
||||
} |
||||
|
||||
if (test_failed) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "=== Reality BGP Test: FAILED ==="); |
||||
return 1; |
||||
} |
||||
DEBUG_INFO(DEBUG_CATEGORY_BGP, "=== Reality BGP Test: PASSED ==="); |
||||
return 0; |
||||
} |
||||
Loading…
Reference in new issue