mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-07-14 08:58:47 +00:00
Finalmask: Add Realm (UDP hole punching in Hysteria v2.9.1) (#6137)
https://github.com/XTLS/Xray-core/pull/5657#issuecomment-4446406536 https://github.com/XTLS/Xray-core/pull/6137#issuecomment-4469822775 Example: https://github.com/XTLS/Xray-core/pull/6137#issue-4454013510
This commit is contained in:
@@ -0,0 +1,307 @@
|
||||
package realm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/xtls/xray-core/transport/internet/tls"
|
||||
)
|
||||
|
||||
const maxErrorBodySize = 64 * 1024
|
||||
|
||||
const (
|
||||
PunchNonceSize = 16
|
||||
PunchObfsKeySize = 32
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
scheme string
|
||||
hostport string
|
||||
token string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type RegisterResponse struct {
|
||||
SessionID string `json:"session_id"`
|
||||
TTL int `json:"ttl"`
|
||||
}
|
||||
|
||||
type HeartbeatResponse struct {
|
||||
TTL int `json:"ttl"`
|
||||
}
|
||||
|
||||
type HeartbeatRequest struct {
|
||||
Addresses []string `json:"addresses,omitempty"`
|
||||
}
|
||||
|
||||
type PunchMetadata struct {
|
||||
Nonce string `json:"nonce"`
|
||||
Obfs string `json:"obfs"`
|
||||
}
|
||||
|
||||
type ConnectRequest struct {
|
||||
Addresses []string `json:"addresses"`
|
||||
PunchMetadata
|
||||
}
|
||||
|
||||
type ConnectResponse struct {
|
||||
Addresses []string `json:"addresses"`
|
||||
PunchMetadata
|
||||
}
|
||||
|
||||
type PunchEvent struct {
|
||||
Addresses []string `json:"addresses"`
|
||||
PunchMetadata
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type StatusError struct {
|
||||
StatusCode int
|
||||
Response ErrorResponse
|
||||
}
|
||||
|
||||
func (e *StatusError) Error() string {
|
||||
if e.Response.Error != "" || e.Response.Message != "" {
|
||||
return fmt.Sprintf("realm server returned %d: %s: %s", e.StatusCode, e.Response.Error, e.Response.Message)
|
||||
}
|
||||
return fmt.Sprintf("realm server returned %d", e.StatusCode)
|
||||
}
|
||||
|
||||
func NewClient(scheme, host, port, token string, tlsConfig *tls.Config) *Client {
|
||||
client := http.DefaultClient
|
||||
if tlsConfig != nil {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.TLSClientConfig = tlsConfig.GetTLSConfig()
|
||||
client = &http.Client{Transport: tr}
|
||||
}
|
||||
return &Client{
|
||||
scheme: scheme,
|
||||
hostport: net.JoinHostPort(host, port),
|
||||
token: token,
|
||||
httpClient: client,
|
||||
}
|
||||
}
|
||||
|
||||
func NewPunchMetadata() (PunchMetadata, error) {
|
||||
nonce, err := randHex(PunchNonceSize)
|
||||
if err != nil {
|
||||
return PunchMetadata{}, err
|
||||
}
|
||||
obfs, err := randHex(PunchObfsKeySize)
|
||||
if err != nil {
|
||||
return PunchMetadata{}, err
|
||||
}
|
||||
return PunchMetadata{
|
||||
Nonce: nonce,
|
||||
Obfs: obfs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Register(ctx context.Context, realmID string, addresses []string) (*RegisterResponse, error) {
|
||||
var resp RegisterResponse
|
||||
if err := c.doJSON(ctx, http.MethodPost, realmID, "", c.token, addressRequest{Addresses: addresses}, http.StatusOK, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) Deregister(ctx context.Context, realmID, sessionID string) error {
|
||||
return c.doJSON(ctx, http.MethodDelete, realmID, "", sessionID, nil, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (c *Client) Heartbeat(ctx context.Context, realmID, sessionID string, req HeartbeatRequest) (*HeartbeatResponse, error) {
|
||||
var resp HeartbeatResponse
|
||||
if err := c.doJSON(ctx, http.MethodPost, realmID, "heartbeat", sessionID, req, http.StatusOK, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) Connect(ctx context.Context, realmID string, req ConnectRequest) (*ConnectResponse, error) {
|
||||
var resp ConnectResponse
|
||||
if err := c.doJSON(ctx, http.MethodPost, realmID, "connect", c.token, req, http.StatusOK, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
type ConnectResponseRequest struct {
|
||||
Addresses []string `json:"addresses"`
|
||||
}
|
||||
|
||||
func (c *Client) ConnectResponse(ctx context.Context, realmID, sessionID, nonce string, addresses []string) error {
|
||||
subPath := "connects/" + url.PathEscape(nonce)
|
||||
return c.doJSON(ctx, http.MethodPost, realmID, subPath, sessionID,
|
||||
ConnectResponseRequest{Addresses: addresses}, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
func (c *Client) Events(ctx context.Context, realmID, sessionID string) (*EventStream, error) {
|
||||
req, err := c.newRequest(ctx, http.MethodGet, realmID, "events", sessionID, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
defer resp.Body.Close()
|
||||
return nil, decodeStatusError(resp)
|
||||
}
|
||||
return newEventStream(resp), nil
|
||||
}
|
||||
|
||||
type addressRequest struct {
|
||||
Addresses []string `json:"addresses"`
|
||||
}
|
||||
|
||||
func (c *Client) doJSON(ctx context.Context, method, realmID, subPath, token string, in any, expectedStatus int, out any) error {
|
||||
var body io.Reader
|
||||
if in != nil {
|
||||
bs, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = bytes.NewReader(bs)
|
||||
}
|
||||
req, err := c.newRequest(ctx, method, realmID, subPath, token, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if in != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != expectedStatus {
|
||||
return decodeStatusError(resp)
|
||||
}
|
||||
if out == nil || resp.Body == nil {
|
||||
return nil
|
||||
}
|
||||
return json.NewDecoder(resp.Body).Decode(out)
|
||||
}
|
||||
|
||||
func (c *Client) newRequest(ctx context.Context, method, realmID, subPath, token string, body io.Reader) (*http.Request, error) {
|
||||
u := &url.URL{
|
||||
Scheme: c.scheme,
|
||||
Host: c.hostport,
|
||||
Path: joinURLPath("v1", url.PathEscape(realmID), subPath),
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func randHex(size int) (string, error) {
|
||||
b := make([]byte, size)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func joinURLPath(parts ...string) string {
|
||||
var joined []string
|
||||
for _, part := range parts {
|
||||
part = strings.Trim(part, "/")
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
joined = append(joined, part)
|
||||
}
|
||||
return "/" + strings.Join(joined, "/")
|
||||
}
|
||||
|
||||
func decodeStatusError(resp *http.Response) error {
|
||||
var errResp ErrorResponse
|
||||
_ = json.NewDecoder(io.LimitReader(resp.Body, maxErrorBodySize)).Decode(&errResp)
|
||||
return &StatusError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Response: errResp,
|
||||
}
|
||||
}
|
||||
|
||||
type EventStream struct {
|
||||
resp *http.Response
|
||||
scanner *bufio.Scanner
|
||||
}
|
||||
|
||||
func newEventStream(resp *http.Response) *EventStream {
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Buffer(make([]byte, 1024), 1024*1024)
|
||||
return &EventStream{
|
||||
resp: resp,
|
||||
scanner: scanner,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EventStream) Close() error {
|
||||
return s.resp.Body.Close()
|
||||
}
|
||||
|
||||
func (s *EventStream) Next() (*PunchEvent, error) {
|
||||
var eventName string
|
||||
var data strings.Builder
|
||||
for s.scanner.Scan() {
|
||||
line := s.scanner.Text()
|
||||
if line == "" {
|
||||
if eventName == "" && data.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
if eventName != "punch" {
|
||||
eventName = ""
|
||||
data.Reset()
|
||||
continue
|
||||
}
|
||||
var ev PunchEvent
|
||||
if err := json.Unmarshal([]byte(data.String()), &ev); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ev, nil
|
||||
}
|
||||
if strings.HasPrefix(line, ":") {
|
||||
continue
|
||||
}
|
||||
field, value, ok := strings.Cut(line, ":")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
value = strings.TrimPrefix(value, " ")
|
||||
switch field {
|
||||
case "event":
|
||||
eventName = value
|
||||
case "data":
|
||||
if data.Len() > 0 {
|
||||
data.WriteByte('\n')
|
||||
}
|
||||
data.WriteString(value)
|
||||
}
|
||||
}
|
||||
if err := s.scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
Reference in New Issue
Block a user