Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion frontend/src/components/ServerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface ServerCardProps {
isDenied?: boolean;
bps?: number;
ip?: string;
displayIP?: string;
isIPBanned?: boolean;
onBanStatusChange?: (
leaseId: string,
Expand Down Expand Up @@ -70,6 +71,7 @@ export function ServerCard({
isDenied = false,
bps = 0,
ip = "",
displayIP,
isIPBanned = false,
onBanStatusChange,
onBPSChange,
Expand Down Expand Up @@ -376,7 +378,7 @@ export function ServerCard({

{isApproved && ip && (
<div className="text-[11px] text-text-muted">
IP: <span className="font-mono text-foreground">{ip}</span>
IP: <span className="font-mono text-foreground">{displayIP || ip}</span>
{isIPBanned && (
<span className="ml-2 font-medium text-destructive">
(Banned)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/ServerListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ export function ServerListView({
isDenied={adminServer?.isDenied}
bps={adminServer?.bps}
ip={adminServer?.ip}
displayIP={adminServer?.displayIP}
isIPBanned={adminServer?.isIPBanned}
transport={adminServer?.transport}
onBanStatusChange={onBanStatusChange}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/hooks/useAdmin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export interface AdminServer extends BaseServer {
isApproved: boolean;
isDenied: boolean;
ip: string;
displayIP: string;
isIPBanned: boolean;
transport: string;
udpPort: number;
Expand Down Expand Up @@ -109,6 +110,7 @@ function toAdminServer(
isApproved: row.IsApproved || false,
isDenied: row.IsDenied || false,
ip: row.ClientIP || "",
displayIP: row.ReportedIP || row.ClientIP || "",
isIPBanned: row.IsIPBanned || false,
transport: row.Transport || "tcp",
udpPort: row.UDPPort || 0,
Expand Down
1 change: 1 addition & 0 deletions frontend/src/hooks/useSSRData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ServerData {
Name: string;
BPS?: number;
ClientIP: string;
ReportedIP?: string;
Hostname: string;
Metadata: unknown;
Ready: number;
Expand Down
3 changes: 2 additions & 1 deletion portal/api_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ func (s *Server) registerLease(req types.RegisterRequest, clientIP string) (type
FirstSeenAt: now,
LastSeenAt: now,
ClientIP: clientIP,
ReportedIP: utils.SanitizeReportedIP(req.ReportedIP),
UDPEnabled: req.UDPEnabled,
},
ReverseToken: req.ReverseToken,
Expand Down Expand Up @@ -568,7 +569,7 @@ func (s *Server) renewLease(req types.RenewRequest, clientIP string) (types.Rene
if req.TTL > 0 {
ttl = time.Duration(req.TTL) * time.Second
}
record, err := s.registry.Renew(req.LeaseID, req.ReverseToken, ttl, clientIP)
record, err := s.registry.Renew(req.LeaseID, req.ReverseToken, ttl, clientIP, utils.SanitizeReportedIP(req.ReportedIP))
if err != nil {
return types.RenewResponse{}, err
}
Expand Down
5 changes: 4 additions & 1 deletion portal/lease.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func (r *leaseRegistry) Register(record *leaseRecord) error {
return nil
}

func (r *leaseRegistry) Renew(leaseID, reverseToken string, ttl time.Duration, clientIP string) (*leaseRecord, error) {
func (r *leaseRegistry) Renew(leaseID, reverseToken string, ttl time.Duration, clientIP, reportedIP string) (*leaseRecord, error) {
r.mu.Lock()
defer r.mu.Unlock()

Expand All @@ -140,6 +140,9 @@ func (r *leaseRegistry) Renew(leaseID, reverseToken string, ttl time.Duration, c
record.ClientIP = clientIP
r.policy.IPFilter().RegisterLeaseIP(record.ID, clientIP)
}
if strings.TrimSpace(reportedIP) != "" {
record.ReportedIP = reportedIP
}
return record, nil
}

Expand Down
2 changes: 1 addition & 1 deletion portal/lease_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestLeaseRegistryLifecycle(t *testing.T) {
t.Fatalf("Lookup() = %v, %v, want registered lease", lookedUp, ok)
}

renewed, err := registry.Renew(record.ID, record.ReverseToken, time.Minute, "203.0.113.10")
renewed, err := registry.Renew(record.ID, record.ReverseToken, time.Minute, "203.0.113.10", "")
if err != nil {
t.Fatalf("Renew() error = %v", err)
}
Expand Down
28 changes: 18 additions & 10 deletions sdk/api_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,17 @@ const (
var errRelayIncompatible = errors.New("relay is incompatible")

type apiClient struct {
baseURL *url.URL
httpClient *http.Client
rawTLSConfig *tls.Config
dialTimeout time.Duration
requestTimeout time.Duration
rootCAPEM []byte
name string
reverseToken string
metadata types.LeaseMetadata
ownerAddress string
baseURL *url.URL
httpClient *http.Client
rawTLSConfig *tls.Config
dialTimeout time.Duration
requestTimeout time.Duration
rootCAPEM []byte
name string
reverseToken string
metadata types.LeaseMetadata
ownerAddress string
resolvedPublicIP string
}

func newApiClient(relayURL string, cfg ListenerConfig) (*apiClient, error) {
Expand Down Expand Up @@ -103,6 +104,7 @@ func (a *apiClient) registerLease(ctx context.Context, ttl time.Duration, udpEna
TTL: int(ttl / time.Second),
Bootstraps: bootstraps,
UDPEnabled: udpEnabled,
ReportedIP: a.resolvedPublicIP,
}, &resp); err != nil {
return types.RegisterResponse{}, err
}
Expand Down Expand Up @@ -143,6 +145,11 @@ func (a *apiClient) ensureReady(ctx context.Context) error {
a.close()
a.httpClient = httpClient
a.rawTLSConfig = rawTLSConfig

if a.resolvedPublicIP == "" {
a.resolvedPublicIP = utils.ResolvePublicIP(ctx)
}

return nil
}

Expand Down Expand Up @@ -171,6 +178,7 @@ func (a *apiClient) renewLease(ctx context.Context, leaseID string, ttl time.Dur
LeaseID: leaseID,
ReverseToken: a.reverseToken,
TTL: int(ttl / time.Second),
ReportedIP: a.resolvedPublicIP,
}, &types.RenewResponse{})
}

Expand Down
2 changes: 2 additions & 0 deletions types/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type RegisterRequest struct {
TTL int `json:"ttl,omitempty"`
Bootstraps []string `json:"bootstraps,omitempty"`
UDPEnabled bool `json:"udp_enabled,omitempty"`
ReportedIP string `json:"reported_ip,omitempty"`
}

type RegisterResponse struct {
Expand Down Expand Up @@ -102,6 +103,7 @@ type RenewRequest struct {
LeaseID string `json:"lease_id"`
ReverseToken string `json:"reverse_token"`
TTL int `json:"ttl,omitempty"`
ReportedIP string `json:"reported_ip,omitempty"`
}

type RenewResponse struct {
Expand Down
1 change: 1 addition & 0 deletions types/lease.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Lease struct {
Name string
BPS int64
ClientIP string
ReportedIP string
Hostname string
Bootstraps []string
UDPEnabled bool
Expand Down
105 changes: 105 additions & 0 deletions utils/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package utils

import (
"context"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"time"

"github.com/gosuda/portal/v2/types"
)

// ResolvePublicIP attempts to determine the caller's public IP address
// using well-known external services. Returns empty string on failure.
// Best-effort with a short timeout to avoid blocking registration.
func ResolvePublicIP(ctx context.Context) string {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

endpoints := []string{
"https://api.ipify.org",
"https://ifconfig.me/ip",
}
client := &http.Client{Timeout: 3 * time.Second}

for _, endpoint := range endpoints {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
continue
}
req.Header.Set("User-Agent", "portal-tunnel")

resp, err := client.Do(req)
if err != nil {
continue
}

body, readErr := io.ReadAll(io.LimitReader(resp.Body, 256))
_ = resp.Body.Close()
if resp.StatusCode != http.StatusOK || readErr != nil {
continue
}

if candidate := SanitizeReportedIP(string(body)); candidate != "" {
return candidate
}
}

return ""
}

func SanitizeReportedIP(raw string) string {
candidate := strings.TrimSpace(raw)
if candidate == "" {
return ""
}
if net.ParseIP(candidate) == nil {
return ""
}
return candidate
}

func ResolvePortalRelayURLs(ctx context.Context, explicit []string, includeDefaults bool) ([]string, error) {
explicit, err := NormalizeRelayURLs(explicit...)
if err != nil {
return nil, err
}
if !includeDefaults {
return explicit, nil
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, types.PortalRelayRegistryURL, nil)
if err != nil {
return explicit, nil
}

client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
return explicit, nil
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return explicit, nil
}

var registry struct {
Relays []string `json:"relays"`
}
if err := json.NewDecoder(resp.Body).Decode(&registry); err != nil {
return explicit, nil
}

defaults, err := NormalizeRelayURLs(registry.Relays...)
if err != nil {
return explicit, nil
}
if len(defaults) == 0 {
return explicit, nil
}
return MergeRelayURLs(defaults, nil, explicit)
}
30 changes: 30 additions & 0 deletions utils/network_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package utils

import "testing"

func TestSanitizeReportedIP(t *testing.T) {
t.Parallel()

tests := []struct {
name string
raw string
want string
}{
{name: "empty", raw: "", want: ""},
{name: "whitespace", raw: " ", want: ""},
{name: "ipv4", raw: " 203.0.113.10 ", want: "203.0.113.10"},
{name: "ipv6", raw: " 2001:db8::1 ", want: "2001:db8::1"},
{name: "invalid", raw: "not-an-ip", want: ""},
{name: "host port", raw: "203.0.113.10:443", want: ""},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

if got := SanitizeReportedIP(tc.raw); got != tc.want {
t.Fatalf("SanitizeReportedIP(%q) = %q, want %q", tc.raw, got, tc.want)
}
})
}
}
52 changes: 0 additions & 52 deletions utils/registry.go

This file was deleted.

Loading