diff --git a/3.go b/3.go deleted file mode 100644 index 9bb19cc2..00000000 --- a/3.go +++ /dev/null @@ -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 "/" -} diff --git a/src/config_parser.c b/src/config_parser.c index c393289b..f65593e2 100644 --- a/src/config_parser.c +++ b/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); @@ -712,40 +712,73 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c char link_copy[MAX_CONN_NAME_LEN + MAX_ADDR_LEN]; 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) new_link->next = cli->links; cli->links = new_link; - + return 0; } - + if (strcmp(key, "peer_public_key") == 0) { return assign_string(cli->peer_public_key_hex, MAX_KEY_LEN, value); } - + if (strcmp(key, "keepalive") == 0) { cli->keepalive = atoi(value); 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; diff --git a/src/config_parser.h b/src/config_parser.h index 7f54f3bf..0301e154 100644 --- a/src/config_parser.h +++ b/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 { diff --git a/src/transport_layer/etcp_connections.c b/src/transport_layer/etcp_connections.c index 00b335ba..bea2afdb 100644 --- a/src/transport_layer/etcp_connections.c +++ b/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) { diff --git a/src/transport_layer/etcp_connections.h b/src/transport_layer/etcp_connections.h index 8ba17b5c..5b61434d 100644 --- a/src/transport_layer/etcp_connections.h +++ b/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); diff --git a/src/transport_layer/reality.c b/src/transport_layer/reality.c index 4f62f1e2..f16156ef 100644 --- a/src/transport_layer/reality.c +++ b/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; diff --git a/src/transport_layer/reality.h b/src/transport_layer/reality.h index 08df7c79..2b3645d6 100644 --- a/src/transport_layer/reality.h +++ b/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 — итоговый размер. diff --git a/src/utun_instance.c b/src/utun_instance.c index 20a244a7..85641ae7 100644 --- a/src/utun_instance.c +++ b/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) { diff --git a/tests/test_reality_hello.c b/tests/test_reality_hello.c index 47527fb5..b9be94b8 100644 --- a/tests/test_reality_hello.c +++ b/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; diff --git a/utun.conf.sample b/utun.conf.sample index fa082325..0172c4a0 100644 --- a/utun.conf.sample +++ b/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]