Browse Source

reality: конфиг [client] для REALITY-клиентов (server_name/short_id/public_key/version/fingerprint) + TCP-линки через NCD

v2
evgeny 3 weeks ago
parent
commit
c2882c533e
  1. 299
      3.go
  2. 66
      src/config_parser.c
  3. 2
      src/config_parser.h
  4. 74
      src/transport_layer/etcp_connections.c
  5. 5
      src/transport_layer/etcp_connections.h
  6. 9
      src/transport_layer/reality.c
  7. 4
      src/transport_layer/reality.h
  8. 10
      src/utun_instance.c
  9. 54
      tests/test_reality_hello.c
  10. 23
      utun.conf.sample

299
3.go

@ -1,299 +0,0 @@
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 "/"
}

66
src/config_parser.c

@ -700,7 +700,7 @@ static int parse_server(const char *key, const char *value, struct CFG_SERVER *s
return 0;
}
if (strcmp(key, "reality") == 0) {
srv->reality_enabled = atoi(value) ? 1 : 0;
srv->reality_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown server option '%s'. Valid: addr, so_mark, fib, netif, type, mtu, only_local, transport, reality", filename, line_num, key);
@ -713,20 +713,19 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c
if (strlen(value) >= sizeof(link_copy)) return -1;
strcpy(link_copy, value);
// Find first colon (separator between server and ip:port)
// Первый ':' отделяет (опциональное) имя локального сокета от ip:port.
// Если токен до ':' не является именем [server] — трактуем всю строку как
// ip:port (remote-only, без локального bind-сокета).
char *first_colon = strchr(link_copy, ':');
if (!first_colon) return -1;
*first_colon = '\0';
// Find server by name
struct CFG_SERVER *local_srv = find_server_by_name(servers, link_copy);
if (!local_srv) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "parse_client: server '%s' not found for client link", link_copy);
return -1;
struct CFG_SERVER *local_srv = NULL;
const char *remote = value;
if (first_colon) {
*first_colon = '\0';
local_srv = find_server_by_name(servers, link_copy);
if (local_srv) remote = first_colon + 1;
}
struct CFG_CLIENT_LINK *new_link = create_client_link(local_srv, first_colon + 1);
struct CFG_CLIENT_LINK *new_link = create_client_link(local_srv, remote);
if (!new_link) return -1;
// Add to linked list (prepend)
@ -745,7 +744,41 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown client option '%s'. Valid: link, peer_public_key, keepalive", filename, line_num, key);
if (strcmp(key, "reality") == 0) {
cli->reality_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
return 0;
}
if (strcmp(key, "server_name") == 0) { return assign_string(cli->reality.server_name, sizeof(cli->reality.server_name), value); }
if (strcmp(key, "short_id") == 0) {
if (hex_to_binary(value, cli->reality.short_id, REALITY_SHORT_ID_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: client reality short_id must be %d hex chars", filename, line_num, REALITY_SHORT_ID_SIZE * 2);
return -1;
}
return 0;
}
if (strcmp(key, "public_key") == 0) {
if (hex_to_binary(value, cli->reality.server_static_pubkey, REALITY_AUTH_KEY_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: client reality public_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2);
return -1;
}
return 0;
}
if (strcmp(key, "version") == 0) {
int x = 0, y = 0, z = 0;
if (sscanf(value, "%d.%d.%d", &x, &y, &z) != 3 || x < 0 || x > 255 || y < 0 || y > 255 || z < 0 || z > 255) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad client reality version '%s' (expected x.y.z)", filename, line_num, value);
return -1;
}
cli->reality.version[0] = (uint8_t)x; cli->reality.version[1] = (uint8_t)y; cli->reality.version[2] = (uint8_t)z;
return 0;
}
if (strcmp(key, "fingerprint") == 0) {
if (strcmp(value, "chrome") == 0) { cli->reality.fingerprint = REALITY_FP_CHROME; return 0; }
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: unknown client reality fingerprint '%s' (valid: chrome)", filename, line_num, value);
return -1;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown client option '%s'. Valid: link, peer_public_key, keepalive, reality, server_name, short_id, public_key, version, fingerprint", filename, line_num, key);
return -1;
}
@ -1012,6 +1045,7 @@ static struct utun_config* parse_config_internal(FILE *fp, const char *filename)
goto error;
}
strcpy(cur_client->name, name);
reality_client_config_set_defaults(&cur_client->reality);
} else if (cur_section == SECTION_NETWORK) {
cur_network = u_calloc(1, sizeof(struct CFG_NETWORK));
if (!cur_network) {
@ -1284,7 +1318,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
char name[MAX_CONN_NAME_LEN];
cur_section = parse_section_header(trimmed, name, sizeof(name));
if (cur_section == SECTION_SERVER) { cur_server = u_calloc(1, sizeof(struct CFG_SERVER)); if (!cur_server) goto error; strcpy(cur_server->name, name); cur_server->fib = -1; }
else if (cur_section == SECTION_CLIENT) { cur_client = u_calloc(1, sizeof(struct CFG_CLIENT)); if (!cur_client) goto error; strcpy(cur_client->name, name); }
else if (cur_section == SECTION_CLIENT) { cur_client = u_calloc(1, sizeof(struct CFG_CLIENT)); if (!cur_client) goto error; strcpy(cur_client->name, name); reality_client_config_set_defaults(&cur_client->reality); }
else if (cur_section == SECTION_NETWORK) { cur_network = u_calloc(1, sizeof(struct CFG_NETWORK)); if (!cur_network) goto error; strcpy(cur_network->name, name); }
continue;
}
@ -1453,12 +1487,12 @@ void print_config(const struct utun_config *cfg) {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Clients:");
struct CFG_CLIENT *c = cfg->clients;
while (c) {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " %s: peer_key=%s, keepalive=%d", c->name, c->peer_public_key_hex, c->keepalive);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " %s: peer_key=%s, keepalive=%d, reality=%d", c->name, c->peer_public_key_hex, c->keepalive, c->reality_enabled);
struct CFG_CLIENT_LINK *link = c->links;
while (link) {
ip_str_t addr_str = sockaddr_storage_to_str(&link->remote_addr);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, " Link: %s (via %s)",
addr_str.str, link->local_srv->name);
addr_str.str, link->local_srv ? link->local_srv->name : "auto");
link = link->next;
}
c = c->next;

2
src/config_parser.h

@ -68,6 +68,8 @@ struct CFG_CLIENT {
int keepalive;
struct CFG_CLIENT_LINK *links; // Linked list of links
struct CFG_CLIENT *next; // Next client in linked list
uint8_t reality_enabled; // 1 = TCP-линк с REALITY-камуфляжем
struct reality_client_config reality; // short_id, server_static_pubkey, version, server_name, fingerprint
};
struct CFG_NETWORK {

74
src/transport_layer/etcp_connections.c

@ -1081,15 +1081,16 @@ static void tcp_link_reconnect_cb(void *arg) {
void etcp_tcp_link_start_connect(struct ETCP_LINK *link, struct sockaddr_storage *addr, uint16_t port) {
if (!link || !link->etcp || !addr) return;
if (!link->conn) { for (struct ETCP_SOCKET *s = link->etcp->instance->etcp_sockets; s; s = s->next) { if (s->is_tcp) { link->conn = s; break; } }
if (!link->conn) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "[%s] no TCP socket in instance, cannot start TCP link %d", link->etcp->log_name, link->local_link_id); return; } }
/* link->conn опционален: NULL = без локального bind (ОС выбирает source IP/интерфейс).
* Задан — bind к interface_addr этого сокета (выбор интерфейса/ip). */
memcpy(&link->remote_addr, addr, sizeof(*addr));
struct stcp_link *sl = stcp_link_connect(link, &link->remote_addr, port);
if (!sl) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "etcp_tcp_link_start_connect: stcp_link_connect failed"); return; }
link->tcp_link = sl;
if (link->is_server == 0) stcp_link_set_on_close(sl, tcp_link_close_cb, link);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] TCP link %d → stcp_link_connect %s:%u rc=%p",
link->etcp->log_name, link->local_link_id, addr, port, (void*)sl);
DEBUG_INFO(DEBUG_CATEGORY_ETCP, "[%s] TCP link %d → stcp_link_connect %s:%u rc=%p bind=%s",
link->etcp->log_name, link->local_link_id, addr, port, (void*)sl,
link->conn ? sockaddr_storage_to_str(&link->conn->interface_addr).str : "auto");
}
void etcp_tcp_link_start_reconnect(struct ETCP_LINK *link) {
@ -2319,13 +2320,13 @@ int etcp_packet_decrypted(struct ETCP_SOCKET* e_sock, struct ETCP_DGRAM* pkt,
}
if (link->link_state == LINK_STATE_CONNECTED) {
if (memory_pool_is_freed(e_sock->instance->pkt_pool, pkt)) {
if (memory_pool_is_freed(link->etcp->instance->pkt_pool, pkt)) {
DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "pkt=%p ALREADY FREED in pkt_pool — HALTING", (void*)pkt);
volatile int _halt = 1; while (_halt) {}
}
etcp_conn_input(pkt);
} else {
memory_pool_free(e_sock->instance->pkt_pool, pkt);
memory_pool_free(link->etcp->instance->pkt_pool, pkt);
}
return 0;
}
@ -2466,6 +2467,55 @@ int init_sockets(struct UTUN_INSTANCE* instance) {
return 0; // All OK
}
/* Создать TCP-линк(и) с REALITY-камуфляжем для [client] с reality=1.
* conn создаётся через NCD (только public_key, без адресов — линки добавляем сами).
* local_srv у link опционален: если задан — это [server] transport=tcp для bind
* на нужный интерфейс (ip); если нет — bind не делается (ОС выбирает source).
* Возвращает NCD-handle (сохранить в config_conn_handles) или NULL. */
struct NODE_CONN_DIRECT* etcp_config_client_reality(struct UTUN_INSTANCE* instance, struct CFG_CLIENT* client,
const uint8_t pubkey_bin[SC_PUBKEY_SIZE], uint64_t node_id) {
if (!instance || !client || !pubkey_bin) return NULL;
struct TOPO_NODE ni_tmp; memset(&ni_tmp, 0, sizeof(ni_tmp));
ni_tmp.node_id = node_id; ni_tmp.node_name = client->name;
memcpy(ni_tmp.public_key, pubkey_bin, SC_PUBKEY_SIZE);
struct NODE_CONN_DIRECT* handle = NULL;
int r = node_conn_direct_open_node(instance, node_id, NULL, NULL, &handle, &ni_tmp, NULL);
if (r == NCD_ERR || !handle) {
DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "client %s reality: ncd open failed node=0x%016llx", client->name, (unsigned long long)node_id);
return NULL;
}
struct ETCP_CONN* conn = node_conn_direct_get_conn(handle);
if (!conn) { node_conn_direct_close(handle); return NULL; }
int link_count = 0;
for (struct CFG_CLIENT_LINK* cl = client->links; cl; cl = cl->next) {
if (cl->remote_addr.ss_family != AF_INET && cl->remote_addr.ss_family != AF_INET6) {
DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "client %s reality: link without remote addr, skipping", client->name);
continue;
}
struct ETCP_SOCKET* bind_sock = NULL;
if (cl->local_srv) {
for (struct ETCP_SOCKET* s = instance->etcp_sockets; s; s = s->next)
if (strcmp(cl->local_srv->name, s->name) == 0) { bind_sock = s; break; }
if (!bind_sock) DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "client %s reality: bind socket '%s' not found, using auto", client->name, cl->local_srv->name);
else if (!bind_sock->is_tcp) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "client %s reality: bind socket '%s' is not TCP, ignoring bind", client->name, cl->local_srv->name); bind_sock = NULL; }
}
struct ETCP_LINK* tlink = etcp_link_new(conn, bind_sock, &cl->remote_addr, 0);
if (!tlink) { DEBUG_ERROR(DEBUG_CATEGORY_CONNECTION, "client %s reality: etcp_link_new failed", client->name); continue; }
tlink->is_tcp = 1;
tlink->reality = client->reality; tlink->reality_set = 1;
etcp_tcp_link_start_connect(tlink, &cl->remote_addr, 0);
link_count++;
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "client %s reality: TCP link %d → %s sn=%s bind=%s",
client->name, tlink->local_link_id, sockaddr_storage_to_str(&cl->remote_addr).str,
client->reality.server_name, bind_sock ? bind_sock->name : "auto");
}
if (link_count == 0) DEBUG_WARN(DEBUG_CATEGORY_CONNECTION, "client %s reality: no links created", client->name);
return handle;
}
int init_connections(struct UTUN_INSTANCE* instance) {
DEBUG_TRACE(DEBUG_CATEGORY_CONNECTION, "");
if (!instance || !instance->config) return -1;
@ -2505,6 +2555,18 @@ int init_connections(struct UTUN_INSTANCE* instance) {
uint64_t node_id = sc_derive_node_id_from_pubkey(pubkey_bin);
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "client %s node_id=0x%016llx", client->name, (unsigned long long)node_id);
if (client->reality_enabled) {
struct NODE_CONN_DIRECT* rhandle = etcp_config_client_reality(instance, client, pubkey_bin, node_id);
if (rhandle) {
struct CONFIG_CONN_HANDLE* ch = u_calloc(1, sizeof(*ch));
if (ch) { ch->node_id = node_id; strncpy(ch->name, client->name, MAX_CONN_NAME_LEN - 1);
ch->handle = rhandle; ch->next = instance->config_conn_handles;
instance->config_conn_handles = ch;
DEBUG_DEBUG(DEBUG_CATEGORY_CONNECTION, "client %s saved reality handle to config_conn_handles", client->name); }
}
client = client->next; continue;
}
struct NODE_CONN_DIRECT* handle = NULL;
struct ETCP_CONN* conn = NULL;
for (struct CFG_CLIENT_LINK* cl = client->links; cl; cl = cl->next) {

5
src/transport_layer/etcp_connections.h

@ -341,6 +341,11 @@ int init_sockets(struct UTUN_INSTANCE* instance);
// Создаёт listen-сокеты и client connections из конфига
int init_connections(struct UTUN_INSTANCE* instance);
// Создать TCP-линк(и) с REALITY-камуфляжем для [client] с reality=1.
// local_srv у link опционален (bind на интерфейс); возвращает NCD-handle или NULL.
struct NODE_CONN_DIRECT* etcp_config_client_reality(struct UTUN_INSTANCE* instance, struct CFG_CLIENT* client,
const uint8_t pubkey_bin[SC_PUBKEY_SIZE], uint64_t node_id);
// SOCKET FUNCTIONS
// добавляет новый версер (сокет для приёма и отправки кодограмм. обслуживает много подключений)
struct ETCP_SOCKET* etcp_socket_add(struct UTUN_INSTANCE* instance, struct CFG_SERVER* server);

9
src/transport_layer/reality.c

@ -119,6 +119,15 @@ void reality_config_set_defaults(struct reality_config *cfg) {
cfg->fingerprint = REALITY_FP_CHROME;
}
void reality_client_config_set_defaults(struct reality_client_config *cfg) {
if (!cfg) return;
memset(cfg->server_static_pubkey, 0, sizeof(cfg->server_static_pubkey));
memset(cfg->short_id, 0, sizeof(cfg->short_id));
cfg->version[0] = 1; cfg->version[1] = 0; cfg->version[2] = 0;
cfg->server_name[0] = '\0';
cfg->fingerprint = REALITY_FP_CHROME;
}
static int reality_x25519(const uint8_t priv[32], const uint8_t pub[32], uint8_t shared[32]) {
EVP_PKEY *pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, priv, 32);
if (!pkey) return -1;

4
src/transport_layer/reality.h

@ -82,6 +82,10 @@ struct reality_config {
// (version=1.0.0, fingerprint=chrome, time_window=30). Вызывается при инициализации.
void reality_config_set_defaults(struct reality_config *cfg);
// Заполняет клиентскую конфигурацию (version=1.0.0, fingerprint=chrome).
// Вызывается при парсинге reality-ключей в секции [client].
void reality_client_config_set_defaults(struct reality_client_config *cfg);
// Клиент: собрать ClientHello (TLS record + handshake) с REALITY-авторизацией.
// Генерирует эфемерный X25519 и Random внутри. out_cap >= REALITY_MAX_CH_SIZE.
// Возвращает REALITY_OK / REALITY_ERR_*, в *out_len — итоговый размер.

10
src/utun_instance.c

@ -937,6 +937,16 @@ struct UTUN_INSTANCE *utun_instance_reload(struct UTUN_INSTANCE *instance, struc
if (sc_hex_to_binary(nc->peer_public_key_hex, pubkey_bin, SC_PUBKEY_SIZE) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_ETCP, "invalid pubkey hex client %s", nc->name); continue; }
uint64_t node_id = sc_derive_node_id_from_pubkey(pubkey_bin);
if (nc->reality_enabled) {
struct NODE_CONN_DIRECT* rhandle = etcp_config_client_reality(instance, nc, pubkey_bin, node_id);
if (rhandle) {
ch = u_calloc(1, sizeof(*ch));
if (ch) { ch->node_id = node_id; strncpy(ch->name, nc->name, MAX_CONN_NAME_LEN - 1);
ch->handle = rhandle; ch->next = instance->config_conn_handles; instance->config_conn_handles = ch; }
}
continue;
}
struct NODE_CONN_DIRECT* handle = NULL;
struct ETCP_CONN* conn = NULL;
for (struct CFG_CLIENT_LINK *nl = nc->links; nl; nl = nl->next) {

54
tests/test_reality_hello.c

@ -205,6 +205,60 @@ int main(void) {
}
}
// ── Сценарий 11: парсинг reality-ключей в секции [client] ──
{
const char *cfg_text =
"[global]\nname=test\n"
"[server: bind_srv]\naddr=127.0.0.1:2000\ntransport=tcp\n"
"[client: c1]\n"
"peer_public_key=1111111111111111111111111111111111111111111111111111111111111111\n"
"link=1.2.3.4:1443\n"
"reality=yes\n"
"server_name=www.microsoft.com\n"
"short_id=0102030405060708\n"
"public_key=2222222222222222222222222222222222222222222222222222222222222222\n"
"version=2.3.4\n"
"fingerprint=chrome\n"
"[client: c2]\n"
"peer_public_key=1111111111111111111111111111111111111111111111111111111111111111\n"
"link=bind_srv:5.6.7.8:1444\n"
"reality=yes\n"
"short_id=aabbccddeeff0011\n";
struct utun_config *uc = parse_config_from_buf(cfg_text, strlen(cfg_text), "mem");
CHECK(uc != NULL, "parse config with [client] reality");
if (uc) {
/* parser prepends: c2 первая, c1 вторая */
struct CFG_CLIENT *c2 = uc->clients;
struct CFG_CLIENT *c1 = c2 ? c2->next : NULL;
CHECK(c2 && strcmp(c2->name, "c2") == 0, "client c2 present");
CHECK(c1 && strcmp(c1->name, "c1") == 0, "client c1 present");
if (c1) {
CHECK(c1->reality_enabled == 1, "c1 reality enabled");
CHECK(strcmp(c1->reality.server_name, "www.microsoft.com") == 0, "c1 server_name");
CHECK(c1->reality.fingerprint == REALITY_FP_CHROME, "c1 fingerprint");
CHECK(c1->reality.version[0] == 2 && c1->reality.version[1] == 3 && c1->reality.version[2] == 4, "c1 version parsed");
{ uint8_t sid[8] = {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08};
CHECK(memcmp(c1->reality.short_id, sid, 8) == 0, "c1 short_id"); }
{ uint8_t pk[32]; memset(pk, 0x22, 32);
CHECK(memcmp(c1->reality.server_static_pubkey, pk, 32) == 0, "c1 public_key"); }
CHECK(c1->links != NULL && c1->links->local_srv == NULL, "c1 link без bind (local_srv NULL)");
if (c1->links) {
struct sockaddr_in* sin = (struct sockaddr_in*)&c1->links->remote_addr;
CHECK(c1->links->remote_addr.ss_family == AF_INET && ntohs(sin->sin_port) == 1443, "c1 remote 1.2.3.4:1443");
}
}
if (c2) {
CHECK(c2->reality_enabled == 1, "c2 reality enabled");
CHECK(c2->reality.version[0] == 1 && c2->reality.version[1] == 0 && c2->reality.version[2] == 0, "c2 version default 1.0.0");
CHECK(c2->links != NULL && c2->links->local_srv != NULL, "c2 link с bind (local_srv set)");
if (c2->links && c2->links->local_srv)
CHECK(strcmp(c2->links->local_srv->name, "bind_srv") == 0, "c2 bind socket name");
}
free_config(uc);
}
}
if (test_failed) {
DEBUG_ERROR(DEBUG_CATEGORY_REALITY, "=== Reality Hello Test: FAILED ===");
return 1;

23
utun.conf.sample

@ -117,24 +117,39 @@ allow=all
# REALITY маскирует TCP-порт STCP под обычный TLS-трафик к реальному сайту.
# Сервер принимает TLS ClientHello, проверяет авторизацию (short_id + X25519)
# и отвечает ServerHello; неавторизованных клиентов релеит на реальный сайт (dest).
# Для включения нужен TCP-сокет: в [server: ...] задать transport=tcp и reality=1.
# Для включения на сервере нужен TCP-сокет: [server: ...] transport=tcp + reality=yes.
#[reality]
#enabled=1
#server_name=www.microsoft.com # SNI-таргет (каким сайтом прикидываемся)
#dest=www.microsoft.com:443 # host:port реального сайта для релея
#private_key=<64 hex X25519> # static приватный ключ сервера
#short_ids=0102030405060708,aabbccddeeff0011 # список short_id (по 16 hex, через запятую)
##short_id=0102030405060708 # одиночный short_id (альтернатива short_ids)
#version=1.0.0 # версия протокола utun (по умолчанию 1.0.0)
#time_window=30 # допуск timestamp, сек (антиреплей)
#fingerprint=chrome # TLS-отпечаток (только chrome)
## Пример TCP-сокета с reality-камуфляжем:
## Пример TCP-сокета с reality-камуфляжем (сервер):
#[server: reality_srv]
#addr=0.0.0.0:1443
#type=public
#transport=tcp
#reality=1
#reality=yes
## --- Клиент: подключение к reality-сокету ---
## Всё нужное для подключения задаётся прямо в секции [client].
## Серверный сокет (bind) опционален: link=ip:port — ОС сама выберет source;
## чтобы биндиться на конкретный интерфейс/ip, укажи [server: ...] transport=tcp перед ip:port.
## peer_public_key — pubkey УЗЛА сервера (STCP), public_key — reality pubkey сервера.
#[client: to_relay]
#peer_public_key=<64 hex pubkey узла сервера>
#link=1.2.3.4:1443 # без bind (ОС выбирает source)
##link=tcp_bind:1.2.3.4:1443 # bind к интерфейсу [server: tcp_bind] transport=tcp
#reality=yes
#server_name=www.microsoft.com # SNI-таргет (должен совпадать с сервером)
#short_id=0102030405060708 # short_id, выданный сервером
#public_key=<64 hex reality pubkey сервера>
#version=1.0.0 # должна совпадать с версией сервера
#fingerprint=chrome
# --- Chatserver: headless supernode config ---
#[chatserver]

Loading…
Cancel
Save