remove ip logic, fixed for docker-compose cli.

Hee Sung Son committed Nov 18, 2025 at 15:43 UTC de7c3b58dff3efed542b51b04ca951f1df518057
15 files changed +278 -690
Dockerfile
+24 -1
@@ -1,5 +1,25 @@
1 # syntax=docker/dockerfile:1
2
3 +# Frontend build stage
4 +FROM --platform=$BUILDPLATFORM node:22-alpine AS frontend-builder
5 +
6 +WORKDIR /src
7 +
8 +# Copy frontend package files
9 +COPY cmd/relay-server/frontend/package*.json ./cmd/relay-server/frontend/
10 +
11 +# Install frontend dependencies
12 +RUN --mount=type=cache,target=/root/.npm \
13 + cd cmd/relay-server/frontend && \
14 + npm ci
15 +
16 +# Copy frontend source code
17 +COPY cmd/relay-server/frontend ./cmd/relay-server/frontend
18 +
19 +# Build frontend
20 +RUN cd cmd/relay-server/frontend && npm run build
21 +
22 +# Go builder stage
23 FROM --platform=$BUILDPLATFORM golang:1 AS builder
24
25 WORKDIR /src
@@ -12,7 +32,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
32 && rm -rf /var/lib/apt/lists/*
33
34 # Set GOMODCACHE to cache Go modules in cache volume
15 -RUN go env -w GOMODCACHE=/root/.cache/go-build
35 +RUN go env -w GOMODCACHE=/root/.cache/go-build
36
37 # Copy go.mod and go.sum
38 COPY go.mod go.sum ./
@@ -24,6 +44,9 @@ RUN --mount=type=cache,target=/go/pkg/mod \
44 # Copy the rest of the source code
45 COPY . .
46
47 +# Copy built frontend from frontend-builder stage
48 +COPY --from=frontend-builder /src/cmd/relay-server/app ./cmd/relay-server/app
49 +
50 RUN --mount=type=cache,target=/go/pkg/mod \
51 --mount=type=cache,target=/root/.cache/go-build \
52 make build-wasm
Makefile
+2 -2
@@ -85,11 +85,11 @@ compress-wasm:
85 # Build React frontend with Tailwind CSS 4
86 build-frontend:
87 @echo "[frontend] building React frontend..."
88 - @cd cmd/relay-server/frontend && npm run build
88 + @cd cmd/relay-server/frontend && npm i && npm run build
89 @echo "[frontend] build complete"
90
91 # Build Go relay server (embeds WASM from cmd/relay-server/static)
92 -build-server: build-frontend
92 +build-server:
93 @echo "[server] building Go portal..."
94 CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o bin/relay-server ./cmd/relay-server
95
cmd/relay-server/frontend.go
+1 -13
@@ -493,19 +493,7 @@ func isPortalSubdomain(host string) bool {
493 return false
494 }
495
496 - // Remove port from host for comparison
497 - hostWithoutPort := host
498 - if idx := strings.LastIndex(host, ":"); idx != -1 {
499 - hostWithoutPort = host[:idx]
500 - }
501 -
502 - // Remove port from portalHost for comparison
503 - portalHostWithoutPort := portalHost
504 - if idx := strings.LastIndex(portalHost, ":"); idx != -1 {
505 - portalHostWithoutPort = portalHost[:idx]
506 - }
507 -
508 - return strings.HasSuffix(hostWithoutPort, "."+portalHostWithoutPort)
496 + return strings.HasSuffix(host, "."+portalHost)
497 }
498
499 // matchesWildcardPattern checks if a host matches a wildcard pattern (e.g., *.localhost:4017)
cmd/relay-server/frontend/src/App.tsx
+3 -38
@@ -28,26 +28,16 @@ function convertSSRDataToServers(ssrData: ServerData[]) {
28 console.error("[App] Failed to parse metadata:", err, row.Metadata);
29 }
30
31 - // Get region from SSR data (GeoIP-detected region)
32 - const region = row.Region || "unknown";
33 -
34 - // Combine region tag with metadata tags
35 - const metadataTags = Array.isArray(metadata.tags) ? metadata.tags : [];
36 - const allTags =
37 - region !== "unknown" ? [region, ...metadataTags] : metadataTags;
38 -
31 return {
32 id: index + 1,
33 name: row.Name || row.DNS || "(unnamed)",
34 description: metadata.description || "",
43 - tags: allTags,
35 + tags: Array.isArray(metadata.tags) ? metadata.tags : [],
36 thumbnail: metadata.thumbnail || "",
37 owner: metadata.owner || "",
38 online: row.Connected,
39 dns: row.DNS || "",
40 link: row.Link,
49 - region: region,
50 - countryCode: row.CountryCode || "",
41 };
42 });
43 }
@@ -55,7 +45,6 @@ function convertSSRDataToServers(ssrData: ServerData[]) {
45 function App() {
46 const [currentPage, setCurrentPage] = useState(1);
47 const [searchQuery, setSearchQuery] = useState("");
58 - const [region, setRegion] = useState("all");
48 const [status, setStatus] = useState("all");
49 const [sortBy, setSortBy] = useState("default");
50
@@ -75,17 +64,6 @@ function App() {
64 return [];
65 }, [ssrData]);
66
78 - // Extract unique available countries from servers
79 - const availableCountries = useMemo(() => {
80 - const countryCodes = new Set<string>();
81 - servers.forEach((server) => {
82 - if (server.countryCode) {
83 - countryCodes.add(server.countryCode);
84 - }
85 - });
86 - return Array.from(countryCodes).sort();
87 - }, [servers]);
88 -
67 // Filter and sort servers
68 const filteredServers = useMemo(() => {
69 let filtered = servers.filter((server) => {
@@ -98,18 +76,13 @@ function App() {
76 tag.toLowerCase().includes(searchQuery.toLowerCase())
77 );
78
101 - // Country filter (using country code)
102 - const matchesRegion =
103 - region === "all" ||
104 - server.countryCode === region;
105 -
79 // Status filter
80 const matchesStatus =
81 status === "all" ||
82 (status === "online" && server.online) ||
83 (status === "offline" && !server.online);
84
112 - return matchesSearch && matchesRegion && matchesStatus;
85 + return matchesSearch && matchesStatus;
86 });
87
88 // Sort based on sortBy value
@@ -130,7 +103,7 @@ function App() {
103 }
104
105 return filtered;
133 - }, [servers, searchQuery, region, status, sortBy]);
106 + }, [servers, searchQuery, status, sortBy]);
107
108 // Pagination
109 const totalPages = Math.ceil(filteredServers.length / ITEMS_PER_PAGE);
@@ -145,11 +118,6 @@ function App() {
118 setCurrentPage(1);
119 };
120
148 - const handleRegionChange = (value: string) => {
149 - setRegion(value);
150 - setCurrentPage(1);
151 - };
152 -
121 const handleStatusChange = (value: string) => {
122 setStatus(value);
123 setCurrentPage(1);
@@ -170,13 +138,10 @@ function App() {
138 <SearchBar
139 searchQuery={searchQuery}
140 onSearchChange={handleSearchChange}
173 - region={region}
174 - onRegionChange={handleRegionChange}
141 status={status}
142 onStatusChange={handleStatusChange}
143 sortBy={sortBy}
144 onSortByChange={handleSortByChange}
179 - availableCountries={availableCountries}
145 />
146 <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6 p-4 sm:p-6 mt-4">
147 {paginatedServers.length > 0 ? (
cmd/relay-server/frontend/src/components/SearchBar.tsx
-21
@@ -7,30 +7,23 @@ import {
7 SelectTrigger,
8 SelectValue,
9 } from "./ui/select";
10 -import { COUNTRY_NAMES } from "../lib/countries";
10
11 interface SearchBarProps {
12 searchQuery: string;
13 onSearchChange: (value: string) => void;
15 - region: string;
16 - onRegionChange: (value: string) => void;
14 status: string;
15 onStatusChange: (value: string) => void;
16 sortBy: string;
17 onSortByChange: (value: string) => void;
21 - availableCountries: string[];
18 }
19
20 export function SearchBar({
21 searchQuery,
22 onSearchChange,
27 - region,
28 - onRegionChange,
23 status,
24 onStatusChange,
25 sortBy,
26 onSortByChange,
33 - availableCountries,
27 }: SearchBarProps) {
28 return (
29 <div className="space-y-4 px-4 sm:px-6">
@@ -47,20 +40,6 @@ export function SearchBar({
40 </div>
41 </label>
42 <div className="flex flex-wrap gap-3">
50 - <Select value={region} onValueChange={onRegionChange}>
51 - <SelectTrigger className="w-[140px] h-8">
52 - <SelectValue placeholder="Country" />
53 - </SelectTrigger>
54 - <SelectContent>
55 - <SelectItem value="all">All Countries</SelectItem>
56 - {availableCountries.map((countryCode) => (
57 - <SelectItem key={countryCode} value={countryCode}>
58 - {COUNTRY_NAMES[countryCode] || countryCode}
59 - </SelectItem>
60 - ))}
61 - </SelectContent>
62 - </Select>
63 -
43 <Select value={status} onValueChange={onStatusChange}>
44 <SelectTrigger className="w-[140px] h-8">
45 <SelectValue placeholder="Status" />
cmd/relay-server/frontend/src/hooks/useSSRData.ts
-3
@@ -21,9 +21,6 @@ export interface ServerData {
21 StaleRed: boolean;
22 Hide: boolean;
23 Metadata: string;
24 - Region: string;
25 - CountryCode: string;
26 - City: string;
24 }
25
26 /**
cmd/relay-server/frontend/src/lib/countries.ts deleted
-199
@@ -1,199 +0,0 @@
1 -// ISO 3166-1 alpha-2 country code to country name mapping
2 -export const COUNTRY_NAMES: Record<string, string> = {
3 - AF: "Afghanistan",
4 - AL: "Albania",
5 - DZ: "Algeria",
6 - AD: "Andorra",
7 - AO: "Angola",
8 - AR: "Argentina",
9 - AM: "Armenia",
10 - AU: "Australia",
11 - AT: "Austria",
12 - AZ: "Azerbaijan",
13 - BS: "Bahamas",
14 - BH: "Bahrain",
15 - BD: "Bangladesh",
16 - BB: "Barbados",
17 - BY: "Belarus",
18 - BE: "Belgium",
19 - BZ: "Belize",
20 - BJ: "Benin",
21 - BT: "Bhutan",
22 - BO: "Bolivia",
23 - BA: "Bosnia and Herzegovina",
24 - BW: "Botswana",
25 - BR: "Brazil",
26 - BN: "Brunei",
27 - BG: "Bulgaria",
28 - BF: "Burkina Faso",
29 - BI: "Burundi",
30 - KH: "Cambodia",
31 - CM: "Cameroon",
32 - CA: "Canada",
33 - CV: "Cape Verde",
34 - CF: "Central African Republic",
35 - TD: "Chad",
36 - CL: "Chile",
37 - CN: "China",
38 - CO: "Colombia",
39 - KM: "Comoros",
40 - CG: "Congo",
41 - CD: "Congo (DRC)",
42 - CR: "Costa Rica",
43 - HR: "Croatia",
44 - CU: "Cuba",
45 - CY: "Cyprus",
46 - CZ: "Czech Republic",
47 - DK: "Denmark",
48 - DJ: "Djibouti",
49 - DM: "Dominica",
50 - DO: "Dominican Republic",
51 - EC: "Ecuador",
52 - EG: "Egypt",
53 - SV: "El Salvador",
54 - GQ: "Equatorial Guinea",
55 - ER: "Eritrea",
56 - EE: "Estonia",
57 - ET: "Ethiopia",
58 - FJ: "Fiji",
59 - FI: "Finland",
60 - FR: "France",
61 - GA: "Gabon",
62 - GM: "Gambia",
63 - GE: "Georgia",
64 - DE: "Germany",
65 - GH: "Ghana",
66 - GR: "Greece",
67 - GD: "Grenada",
68 - GT: "Guatemala",
69 - GN: "Guinea",
70 - GW: "Guinea-Bissau",
71 - GY: "Guyana",
72 - HT: "Haiti",
73 - HN: "Honduras",
74 - HK: "Hong Kong",
75 - HU: "Hungary",
76 - IS: "Iceland",
77 - IN: "India",
78 - ID: "Indonesia",
79 - IR: "Iran",
80 - IQ: "Iraq",
81 - IE: "Ireland",
82 - IL: "Israel",
83 - IT: "Italy",
84 - JM: "Jamaica",
85 - JP: "Japan",
86 - JO: "Jordan",
87 - KZ: "Kazakhstan",
88 - KE: "Kenya",
89 - KI: "Kiribati",
90 - KP: "North Korea",
91 - KR: "South Korea",
92 - KW: "Kuwait",
93 - KG: "Kyrgyzstan",
94 - LA: "Laos",
95 - LV: "Latvia",
96 - LB: "Lebanon",
97 - LS: "Lesotho",
98 - LR: "Liberia",
99 - LY: "Libya",
100 - LI: "Liechtenstein",
101 - LT: "Lithuania",
102 - LU: "Luxembourg",
103 - MO: "Macau",
104 - MK: "North Macedonia",
105 - MG: "Madagascar",
106 - MW: "Malawi",
107 - MY: "Malaysia",
108 - MV: "Maldives",
109 - ML: "Mali",
110 - MT: "Malta",
111 - MH: "Marshall Islands",
112 - MR: "Mauritania",
113 - MU: "Mauritius",
114 - MX: "Mexico",
115 - FM: "Micronesia",
116 - MD: "Moldova",
117 - MC: "Monaco",
118 - MN: "Mongolia",
119 - ME: "Montenegro",
120 - MA: "Morocco",
121 - MZ: "Mozambique",
122 - MM: "Myanmar",
123 - NA: "Namibia",
124 - NR: "Nauru",
125 - NP: "Nepal",
126 - NL: "Netherlands",
127 - NZ: "New Zealand",
128 - NI: "Nicaragua",
129 - NE: "Niger",
130 - NG: "Nigeria",
131 - NO: "Norway",
132 - OM: "Oman",
133 - PK: "Pakistan",
134 - PW: "Palau",
135 - PS: "Palestine",
136 - PA: "Panama",
137 - PG: "Papua New Guinea",
138 - PY: "Paraguay",
139 - PE: "Peru",
140 - PH: "Philippines",
141 - PL: "Poland",
142 - PT: "Portugal",
143 - QA: "Qatar",
144 - RO: "Romania",
145 - RU: "Russia",
146 - RW: "Rwanda",
147 - KN: "Saint Kitts and Nevis",
148 - LC: "Saint Lucia",
149 - VC: "Saint Vincent",
150 - WS: "Samoa",
151 - SM: "San Marino",
152 - ST: "Sao Tome and Principe",
153 - SA: "Saudi Arabia",
154 - SN: "Senegal",
155 - RS: "Serbia",
156 - SC: "Seychelles",
157 - SL: "Sierra Leone",
158 - SG: "Singapore",
159 - SK: "Slovakia",
160 - SI: "Slovenia",
161 - SB: "Solomon Islands",
162 - SO: "Somalia",
163 - ZA: "South Africa",
164 - SS: "South Sudan",
165 - ES: "Spain",
166 - LK: "Sri Lanka",
167 - SD: "Sudan",
168 - SR: "Suriname",
169 - SZ: "Swaziland",
170 - SE: "Sweden",
171 - CH: "Switzerland",
172 - SY: "Syria",
173 - TW: "Taiwan",
174 - TJ: "Tajikistan",
175 - TZ: "Tanzania",
176 - TH: "Thailand",
177 - TL: "Timor-Leste",
178 - TG: "Togo",
179 - TO: "Tonga",
180 - TT: "Trinidad and Tobago",
181 - TN: "Tunisia",
182 - TR: "Turkey",
183 - TM: "Turkmenistan",
184 - TV: "Tuvalu",
185 - UG: "Uganda",
186 - UA: "Ukraine",
187 - AE: "United Arab Emirates",
188 - GB: "United Kingdom",
189 - US: "United States",
190 - UY: "Uruguay",
191 - UZ: "Uzbekistan",
192 - VU: "Vanuatu",
193 - VA: "Vatican City",
194 - VE: "Venezuela",
195 - VN: "Vietnam",
196 - YE: "Yemen",
197 - ZM: "Zambia",
198 - ZW: "Zimbabwe",
199 -};
cmd/relay-server/static/GeoLite2-Country.mmdb
Binary files a/cmd/relay-server/static/GeoLite2-Country.mmdb and /dev/null differ
cmd/relay-server/view.go
+2 -160
@@ -5,14 +5,11 @@ import (
5 "embed"
6 "encoding/json"
7 "fmt"
8 - "net"
8 "net/http"
9 "strings"
11 - "sync"
10 "time"
11
12 "github.com/gorilla/websocket"
15 - "github.com/oschwald/geoip2-golang"
13 "github.com/rs/zerolog/log"
14
15 "gosuda.org/portal/portal"
@@ -23,16 +20,6 @@ import (
20 //go:embed static
21 var assetsFS embed.FS
22
26 -//go:embed static/GeoLite2-Country.mmdb
27 -var geoipFS embed.FS
28 -
29 -// GeoIP database reader (global instance with lazy initialization)
30 -var (
31 - geoipReader *geoip2.Reader
32 - geoipReaderOnce sync.Once
33 - geoipReaderErr error
34 -)
35 -
23 func serveAsset(mux *http.ServeMux, route, assetPath, contentType string) {
24 mux.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
25 b, err := assetsFS.ReadFile(assetPath)
@@ -73,7 +60,7 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
60 serveAppStatic(w, r, path, serv)
61 })
62
76 - // Portal frontend files (for unified caching)
63 + // Portal frontend files (for unified caching)
64 appMux.HandleFunc("/frontend/", func(w http.ResponseWriter, r *http.Request) {
65 setCORSHeaders(w)
66 if r.Method == http.MethodOptions {
@@ -105,7 +92,7 @@ func serveHTTP(_ context.Context, addr string, serv *portal.RelayServer, nodeID
92 }
93
94 stream := &wsstream.WsStream{Conn: wsConn}
108 - if err := serv.HandleConnection(stream, r.RemoteAddr); err != nil {
95 + if err := serv.HandleConnection(stream); err != nil {
96 log.Error().Err(err).Msg("[server] websocket relay connection error")
97 wsConn.Close()
98 return
@@ -165,121 +152,6 @@ type leaseRow struct {
152 StaleRed bool
153 Hide bool
154 Metadata string
168 - Region string
169 - CountryCode string
170 -}
171 -
172 -// initGeoIP initializes the GeoIP database reader (lazy loaded)
173 -func initGeoIP() error {
174 - geoipReaderOnce.Do(func() {
175 - // Try to load from embedded FS first
176 - data, err := geoipFS.ReadFile("static/GeoLite2-Country.mmdb")
177 - if err == nil {
178 - reader, err := geoip2.FromBytes(data)
179 - if err == nil {
180 - geoipReader = reader
181 - log.Info().
182 - Str("source", "embedded").
183 - Int("size", len(data)).
184 - Msg("GeoIP database loaded successfully from embedded FS")
185 - return
186 - }
187 - log.Warn().Err(err).Msg("Failed to parse embedded GeoIP database")
188 - } else {
189 - log.Debug().Err(err).Msg("Embedded GeoIP database not found, trying file paths")
190 - }
191 - })
192 -
193 - return geoipReaderErr
194 -}
195 -
196 -// getRegionFromIP extracts region information from an IP address
197 -func getRegionFromIP(ipStr string) (region, countryCode string) {
198 - // Initialize GeoIP if needed
199 - if err := initGeoIP(); err != nil {
200 - return "unknown", ""
201 - }
202 -
203 - // Parse IP address
204 - ip := net.ParseIP(ipStr)
205 - if ip == nil {
206 - return "unknown", ""
207 - }
208 -
209 - // Skip private/local IPs
210 - if ip.IsLoopback() || ip.IsPrivate() {
211 - log.Debug().Str("ip", ipStr).Msg("[GeoIP] Skipping local/private IP")
212 - return "local", ""
213 - }
214 -
215 - // Lookup country record
216 - record, err := geoipReader.Country(ip)
217 - if err != nil {
218 - log.Debug().Err(err).Str("ip", ipStr).Msg("GeoIP lookup failed")
219 - return "unknown", ""
220 - }
221 -
222 - // Extract country code
223 - countryCode = record.Country.IsoCode
224 -
225 - // Map country code to region
226 - // Based on common regional groupings used in gaming/CDN services
227 - switch countryCode {
228 - // North America
229 - case "US", "CA", "MX", "GT", "HN", "SV", "NI", "CR", "PA", "BZ":
230 - region = "us-east"
231 -
232 - // South America
233 - case "BR", "AR", "CL", "CO", "PE", "VE", "EC", "BO", "PY", "UY", "GY", "SR", "GF":
234 - region = "south-america"
235 -
236 - // Europe - West
237 - case "GB", "IE", "PT", "ES", "FR", "BE", "NL", "LU":
238 - region = "eu-west"
239 -
240 - // Europe - Central/East
241 - case "DE", "AT", "CH", "IT", "PL", "CZ", "SK", "HU", "RO", "BG", "SI", "HR", "BA", "RS", "ME", "AL", "MK", "GR", "CY":
242 - region = "eu-central"
243 -
244 - // Europe - North
245 - case "SE", "NO", "DK", "FI", "IS", "EE", "LV", "LT":
246 - region = "eu-west"
247 -
248 - // Asia - East
249 - case "JP", "KR", "CN", "TW", "HK", "MO":
250 - region = "asia-pacific"
251 -
252 - // Asia - Southeast
253 - case "SG", "MY", "TH", "ID", "PH", "VN", "LA", "KH", "MM", "BN":
254 - region = "asia-pacific"
255 -
256 - // Asia - South
257 - case "IN", "PK", "BD", "LK", "NP", "BT", "MV":
258 - region = "asia-pacific"
259 -
260 - // Oceania
261 - case "AU", "NZ", "FJ", "PG", "NC", "PF", "SB", "VU", "WS", "TO":
262 - region = "asia-pacific"
263 -
264 - // Middle East
265 - case "AE", "SA", "IL", "TR", "EG", "IQ", "IR", "JO", "LB", "SY", "YE", "OM", "KW", "BH", "QA":
266 - region = "eu-central"
267 -
268 - // Africa
269 - case "ZA", "NG", "KE", "GH", "TZ", "UG", "ET", "MA", "DZ", "TN", "LY", "SD", "AO", "MZ", "ZW", "BW", "NA", "ZM", "MW", "MG":
270 - region = "eu-west"
271 -
272 - default:
273 - region = "unknown"
274 - }
275 -
276 - log.Debug().
277 - Str("ip", ipStr).
278 - Str("region", region).
279 - Str("country", countryCode).
280 - Msg("[GeoIP] Region mapped successfully")
281 -
282 - return region, countryCode
155 }
156
157 // convertLeaseEntriesToRows converts LeaseEntry data from LeaseManager to leaseRow format for the app page
@@ -387,34 +259,6 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
259 link = fmt.Sprintf("//%s.%s/", lease.Name, portalHost)
260 }
261
390 - // Get GeoIP information from RemoteAddr
391 - var region string
392 - var countryCode string
393 - if leaseEntry.RemoteAddr != "" {
394 - // Extract IP from RemoteAddr (format: "ip:port")
395 - ipStr := leaseEntry.RemoteAddr
396 - if idx := strings.LastIndex(ipStr, ":"); idx != -1 {
397 - ipStr = ipStr[:idx]
398 - }
399 - log.Info().
400 - Str("lease_id", identityID).
401 - Str("remote_addr", leaseEntry.RemoteAddr).
402 - Str("extracted_ip", ipStr).
403 - Msg("[GeoIP] Processing lease RemoteAddr")
404 - region, countryCode = getRegionFromIP(ipStr)
405 - log.Info().
406 - Str("lease_id", identityID).
407 - Str("region", region).
408 - Str("country_code", countryCode).
409 - Msg("[GeoIP] Region detected")
410 - } else {
411 - log.Info().
412 - Str("lease_id", identityID).
413 - Msg("[GeoIP] No RemoteAddr available for lease")
414 - region = "unknown"
415 - countryCode = ""
416 - }
417 -
262 row := leaseRow{
263 Peer: identityID,
264 Name: name,
@@ -428,8 +272,6 @@ func convertLeaseEntriesToRows(serv *portal.RelayServer) []leaseRow {
272 StaleRed: !connected && since >= 15*time.Second,
273 Hide: meta.Hide,
274 Metadata: lease.Metadata,
431 - Region: region,
432 - CountryCode: countryCode,
275 }
276
277 if row.Hide != true {
portal/core/cryptoops/handshaker.go
-3
@@ -190,9 +190,6 @@ func (sc *SecureConnection) writeFragmentation(p []byte) (int, error) {
190
191 // Read reads and decrypts data from the underlying connection
192 func (sc *SecureConnection) Read(p []byte) (int, error) {
193 - if sc == nil || sc.conn == nil {
194 - return 0, errors.New("connection is nil")
195 - }
193 if len(sc.readBuffer.B) > 0 {
194 n := copy(p, sc.readBuffer.B)
195 copy(sc.readBuffer.B[:len(sc.readBuffer.B)-n], sc.readBuffer.B[n:])
portal/handlers.go
+1 -1
@@ -65,7 +65,7 @@ func (g *RelayServer) handleLeaseUpdateRequest(ctx *StreamContext, packet *rdver
65 var resp rdverb.LeaseUpdateResponse
66
67 // Update lease in lease manager
68 - if g.leaseManager.UpdateLease(req.Lease, ctx.ConnectionID, ctx.Connection.RemoteAddr) {
68 + if g.leaseManager.UpdateLease(req.Lease, ctx.ConnectionID) {
69 resp.Code = rdverb.ResponseCode_RESPONSE_CODE_ACCEPTED
70
71 // Register lease connection
portal/lease.go
+2 -4
@@ -13,8 +13,7 @@ type LeaseEntry struct {
13 Lease *rdverb.Lease
14 Expires time.Time
15 LastSeen time.Time
16 - ConnectionID int64 // Store the connection ID
17 - RemoteAddr string // Store the remote IP address
16 + ConnectionID int64 // Store the connection ID
17 }
18
19 type LeaseManager struct {
@@ -79,7 +78,7 @@ func (lm *LeaseManager) cleanupExpiredLeases() {
78 }
79 }
80
82 -func (lm *LeaseManager) UpdateLease(lease *rdverb.Lease, connectionID int64, remoteAddr string) bool {
81 +func (lm *LeaseManager) UpdateLease(lease *rdverb.Lease, connectionID int64) bool {
82 lm.leasesLock.Lock()
83 defer lm.leasesLock.Unlock()
84
@@ -129,7 +128,6 @@ func (lm *LeaseManager) UpdateLease(lease *rdverb.Lease, connectionID int64, rem
128 Expires: expires,
129 LastSeen: time.Now(),
130 ConnectionID: connectionID,
132 - RemoteAddr: remoteAddr,
131 }
132
133 // Apply default BPS limit for this lease if configured and no explicit limit set
portal/lease_test.go
+10 -10
@@ -40,12 +40,12 @@ func TestLeaseManager_NameConflict(t *testing.T) {
40 }
41
42 // First lease should succeed
43 - if !lm.UpdateLease(lease1, 1, "192.168.1.1:1234") {
43 + if !lm.UpdateLease(lease1, 1) {
44 t.Fatal("First lease registration should succeed")
45 }
46
47 // Second lease with same name should fail (name conflict)
48 - if lm.UpdateLease(lease2, 2, "192.168.1.2:1234") {
48 + if lm.UpdateLease(lease2, 2) {
49 t.Fatal("Second lease registration should fail due to name conflict")
50 }
51
@@ -91,12 +91,12 @@ func TestLeaseManager_SameIdentityUpdate(t *testing.T) {
91 }
92
93 // First registration
94 - if !lm.UpdateLease(lease1, 1, "192.168.1.1:1234") {
94 + if !lm.UpdateLease(lease1, 1) {
95 t.Fatal("First lease registration should succeed")
96 }
97
98 // Update with same identity should succeed (no conflict)
99 - if !lm.UpdateLease(lease2, 1, "192.168.1.1:1234") {
99 + if !lm.UpdateLease(lease2, 1) {
100 t.Fatal("Updating own lease should succeed")
101 }
102
@@ -139,11 +139,11 @@ func TestLeaseManager_EmptyNameAllowed(t *testing.T) {
139 Expires: time.Now().Add(10 * time.Minute).Unix(),
140 }
141
142 - if !lm.UpdateLease(lease1, 1, "192.168.1.1:1234") {
142 + if !lm.UpdateLease(lease1, 1) {
143 t.Fatal("First lease with empty name should succeed")
144 }
145
146 - if !lm.UpdateLease(lease2, 2, "192.168.1.2:1234") {
146 + if !lm.UpdateLease(lease2, 2) {
147 t.Fatal("Second lease with empty name should succeed (empty names don't conflict)")
148 }
149 }
@@ -177,11 +177,11 @@ func TestLeaseManager_UnnamedAllowed(t *testing.T) {
177 Expires: time.Now().Add(10 * time.Minute).Unix(),
178 }
179
180 - if !lm.UpdateLease(lease1, 1, "192.168.1.1:1234") {
180 + if !lm.UpdateLease(lease1, 1) {
181 t.Fatal("First lease with '(unnamed)' should succeed")
182 }
183
184 - if !lm.UpdateLease(lease2, 2, "192.168.1.2:1234") {
184 + if !lm.UpdateLease(lease2, 2) {
185 t.Fatal("Second lease with '(unnamed)' should succeed (unnamed don't conflict)")
186 }
187 }
@@ -215,11 +215,11 @@ func TestLeaseManager_UnicodeNameConflict(t *testing.T) {
215 Expires: time.Now().Add(10 * time.Minute).Unix(),
216 }
217
218 - if !lm.UpdateLease(lease1, 1, "192.168.1.1:1234") {
218 + if !lm.UpdateLease(lease1, 1) {
219 t.Fatal("First lease with Korean name should succeed")
220 }
221
222 - if lm.UpdateLease(lease2, 2, "192.168.1.2:1234") {
222 + if lm.UpdateLease(lease2, 2) {
223 t.Fatal("Second lease with same Korean name should fail")
224 }
225 }
portal/relay.go
+6 -8
@@ -14,9 +14,8 @@ import (
14 )
15
16 type Connection struct {
17 - conn io.ReadWriteCloser
18 - sess *yamux.Session
19 - RemoteAddr string
17 + conn io.ReadWriteCloser
18 + sess *yamux.Session
19
20 streams map[uint32]*yamux.Stream
21 streamsLock sync.Mutex
@@ -254,7 +253,7 @@ func (g *RelayServer) handleStream(stream *yamux.Stream, id int64, connection *C
253 }
254 }
255
257 -func (g *RelayServer) HandleConnection(conn io.ReadWriteCloser, remoteAddr string) error {
256 +func (g *RelayServer) HandleConnection(conn io.ReadWriteCloser) error {
257 log.Debug().Msg("[RelayServer] New connection received")
258
259 sess, err := yamux.Server(conn, _yamux_config)
@@ -267,10 +266,9 @@ func (g *RelayServer) HandleConnection(conn io.ReadWriteCloser, remoteAddr strin
266 g.connidCounter++
267 connID := g.connidCounter
268 connection := &Connection{
270 - conn: conn,
271 - sess: sess,
272 - RemoteAddr: remoteAddr,
273 - streams: make(map[uint32]*yamux.Stream),
269 + conn: conn,
270 + sess: sess,
271 + streams: make(map[uint32]*yamux.Stream),
272 }
273 g.connections[connID] = connection
274 g.connectionsLock.Unlock()
sdk/types.go
+227 -227
@@ -1,227 +1,227 @@
1 -package sdk
2 -
3 -import (
4 - "context"
5 - "errors"
6 - "io"
7 - "net"
8 - "sync"
9 - "time"
10 -
11 - "github.com/rs/zerolog/log"
12 - "gosuda.org/portal/portal"
13 - "gosuda.org/portal/portal/core/cryptoops"
14 - "gosuda.org/portal/portal/core/proto/rdverb"
15 -)
16 -
17 -var (
18 - ErrNoAvailableRelay = errors.New("no available relay")
19 - ErrClientClosed = errors.New("client is closed")
20 - ErrListenerExists = errors.New("listener already exists for this credential")
21 - ErrRelayExists = errors.New("relay already exists")
22 - ErrRelayNotFound = errors.New("relay not found")
23 - ErrInvalidName = errors.New("lease name contains invalid characters (only alphanumeric, hyphen, underscore allowed)")
24 - ErrFailedToCreateClient = errors.New("failed to create relay client")
25 - ErrInvalidMetadata = errors.New("invalid metadata")
26 -)
27 -
28 -type ClientConfig struct {
29 - BootstrapServers []string
30 - Dialer func(context.Context, string) (io.ReadWriteCloser, error)
31 - HealthCheckInterval time.Duration // Interval for health checks (default: 10 seconds)
32 - ReconnectMaxRetries int // Maximum reconnection attempts (default: 0 = infinite)
33 - ReconnectInterval time.Duration // Interval between reconnection attempts (default: 5 seconds)
34 -}
35 -
36 -type ClientOption func(*ClientConfig)
37 -
38 -func WithBootstrapServers(servers []string) ClientOption {
39 - return func(c *ClientConfig) {
40 - c.BootstrapServers = servers
41 - }
42 -}
43 -
44 -func WithDialer(dialer func(context.Context, string) (io.ReadWriteCloser, error)) ClientOption {
45 - return func(c *ClientConfig) {
46 - c.Dialer = dialer
47 - }
48 -}
49 -
50 -func WithHealthCheckInterval(interval time.Duration) ClientOption {
51 - return func(c *ClientConfig) {
52 - c.HealthCheckInterval = interval
53 - }
54 -}
55 -
56 -func WithReconnectMaxRetries(retries int) ClientOption {
57 - return func(c *ClientConfig) {
58 - c.ReconnectMaxRetries = retries
59 - }
60 -}
61 -
62 -func WithReconnectInterval(interval time.Duration) ClientOption {
63 - return func(c *ClientConfig) {
64 - c.ReconnectInterval = interval
65 - }
66 -}
67 -
68 -type Metadata struct {
69 - Description string `json:"description"`
70 - Tags []string `json:"tags"`
71 - Thumbnail string `json:"thumbnail"`
72 - Owner string `json:"owner"`
73 - Hide bool `json:"hide"`
74 -}
75 -
76 -func (m Metadata) isEmpty() bool {
77 - return m.Description == "" &&
78 - len(m.Tags) == 0 &&
79 - m.Thumbnail == "" &&
80 - m.Owner == ""
81 -}
82 -
83 -type MetadataOption func(*Metadata)
84 -
85 -func WithDescription(description string) MetadataOption {
86 - return func(m *Metadata) {
87 - m.Description = description
88 - }
89 -}
90 -
91 -func WithTags(tags []string) MetadataOption {
92 - return func(m *Metadata) {
93 - m.Tags = tags
94 - }
95 -}
96 -
97 -func WithThumbnail(thumbnail string) MetadataOption {
98 - return func(m *Metadata) {
99 - m.Thumbnail = thumbnail
100 - }
101 -}
102 -
103 -func WithOwner(owner string) MetadataOption {
104 - return func(m *Metadata) {
105 - m.Owner = owner
106 - }
107 -}
108 -
109 -func WithHide(hide bool) MetadataOption {
110 - return func(m *Metadata) {
111 - m.Hide = hide
112 - }
113 -}
114 -
115 -type listener struct {
116 - mu sync.Mutex
117 -
118 - cred *cryptoops.Credential
119 - lease *rdverb.Lease
120 -
121 - conns map[*connection]struct{}
122 -
123 - connCh chan *connection
124 - closed bool
125 -}
126 -
127 -// Implement net.Listener interface for Listener
128 -func (l *listener) Accept() (net.Conn, error) {
129 - conn, ok := <-l.connCh
130 - if !ok {
131 - return nil, net.ErrClosed
132 - }
133 - return conn, nil
134 -}
135 -
136 -func (l *listener) Close() error {
137 - l.mu.Lock()
138 - defer l.mu.Unlock()
139 -
140 - if l.closed {
141 - return nil
142 - }
143 -
144 - l.closed = true
145 -
146 - // Close the connection channel first to prevent new connections
147 - close(l.connCh)
148 -
149 - // Close all active connections
150 - for conn := range l.conns {
151 - if err := conn.Close(); err != nil {
152 - log.Error().Err(err).Msg("[SDK] Error closing connection")
153 - }
154 - delete(l.conns, conn)
155 - }
156 -
157 - // Clear the connections map
158 - l.conns = make(map[*connection]struct{})
159 -
160 - return nil
161 -}
162 -
163 -func (l *listener) Addr() net.Addr {
164 - return addr(l.cred.ID())
165 -}
166 -
167 -type connRelay struct {
168 - addr string
169 - client *portal.RelayClient
170 - dialer func(context.Context, string) (io.ReadWriteCloser, error)
171 - stop chan struct{}
172 - stopOnce sync.Once // Ensure stop channel is closed only once
173 - mu sync.Mutex
174 -}
175 -
176 -var _ net.Conn = (*connection)(nil)
177 -
178 -type connection struct {
179 - via *connRelay
180 - localAddr string
181 - remoteAddr string
182 - conn *cryptoops.SecureConnection
183 -}
184 -
185 -func (r *connection) Read(b []byte) (n int, err error) {
186 - return r.conn.Read(b)
187 -}
188 -
189 -func (r *connection) Write(b []byte) (n int, err error) {
190 - return r.conn.Write(b)
191 -}
192 -
193 -func (r *connection) Close() error {
194 - return r.conn.Close()
195 -}
196 -
197 -func (r *connection) LocalAddr() net.Addr {
198 - return addr(r.localAddr)
199 -}
200 -
201 -func (r *connection) RemoteAddr() net.Addr {
202 - return addr(r.remoteAddr)
203 -}
204 -
205 -func (r *connection) SetDeadline(t time.Time) error {
206 - return r.conn.SetDeadline(t)
207 -}
208 -
209 -func (r *connection) SetReadDeadline(t time.Time) error {
210 - return r.conn.SetReadDeadline(t)
211 -}
212 -
213 -func (r *connection) SetWriteDeadline(t time.Time) error {
214 - return r.conn.SetWriteDeadline(t)
215 -}
216 -
217 -var _ net.Addr = (*addr)(nil)
218 -
219 -type addr string
220 -
221 -func (a addr) Network() string {
222 - return "portal"
223 -}
224 -
225 -func (a addr) String() string {
226 - return string(a)
227 -}
1 +package sdk
2 +
3 +import (
4 + "context"
5 + "errors"
6 + "io"
7 + "net"
8 + "sync"
9 + "time"
10 +
11 + "github.com/rs/zerolog/log"
12 + "gosuda.org/portal/portal"
13 + "gosuda.org/portal/portal/core/cryptoops"
14 + "gosuda.org/portal/portal/core/proto/rdverb"
15 +)
16 +
17 +var (
18 + ErrNoAvailableRelay = errors.New("no available relay")
19 + ErrClientClosed = errors.New("client is closed")
20 + ErrListenerExists = errors.New("listener already exists for this credential")
21 + ErrRelayExists = errors.New("relay already exists")
22 + ErrRelayNotFound = errors.New("relay not found")
23 + ErrInvalidName = errors.New("lease name contains invalid characters (only alphanumeric, hyphen, underscore allowed)")
24 + ErrFailedToCreateClient = errors.New("failed to create relay client")
25 + ErrInvalidMetadata = errors.New("invalid metadata")
26 +)
27 +
28 +type ClientConfig struct {
29 + BootstrapServers []string
30 + Dialer func(context.Context, string) (io.ReadWriteCloser, error)
31 + HealthCheckInterval time.Duration // Interval for health checks (default: 10 seconds)
32 + ReconnectMaxRetries int // Maximum reconnection attempts (default: 0 = infinite)
33 + ReconnectInterval time.Duration // Interval between reconnection attempts (default: 5 seconds)
34 +}
35 +
36 +type ClientOption func(*ClientConfig)
37 +
38 +func WithBootstrapServers(servers []string) ClientOption {
39 + return func(c *ClientConfig) {
40 + c.BootstrapServers = servers
41 + }
42 +}
43 +
44 +func WithDialer(dialer func(context.Context, string) (io.ReadWriteCloser, error)) ClientOption {
45 + return func(c *ClientConfig) {
46 + c.Dialer = dialer
47 + }
48 +}
49 +
50 +func WithHealthCheckInterval(interval time.Duration) ClientOption {
51 + return func(c *ClientConfig) {
52 + c.HealthCheckInterval = interval
53 + }
54 +}
55 +
56 +func WithReconnectMaxRetries(retries int) ClientOption {
57 + return func(c *ClientConfig) {
58 + c.ReconnectMaxRetries = retries
59 + }
60 +}
61 +
62 +func WithReconnectInterval(interval time.Duration) ClientOption {
63 + return func(c *ClientConfig) {
64 + c.ReconnectInterval = interval
65 + }
66 +}
67 +
68 +type Metadata struct {
69 + Description string `json:"description"`
70 + Tags []string `json:"tags"`
71 + Thumbnail string `json:"thumbnail"`
72 + Owner string `json:"owner"`
73 + Hide bool `json:"hide"`
74 +}
75 +
76 +func (m Metadata) isEmpty() bool {
77 + return m.Description == "" &&
78 + len(m.Tags) == 0 &&
79 + m.Thumbnail == "" &&
80 + m.Owner == ""
81 +}
82 +
83 +type MetadataOption func(*Metadata)
84 +
85 +func WithDescription(description string) MetadataOption {
86 + return func(m *Metadata) {
87 + m.Description = description
88 + }
89 +}
90 +
91 +func WithTags(tags []string) MetadataOption {
92 + return func(m *Metadata) {
93 + m.Tags = tags
94 + }
95 +}
96 +
97 +func WithThumbnail(thumbnail string) MetadataOption {
98 + return func(m *Metadata) {
99 + m.Thumbnail = thumbnail
100 + }
101 +}
102 +
103 +func WithOwner(owner string) MetadataOption {
104 + return func(m *Metadata) {
105 + m.Owner = owner
106 + }
107 +}
108 +
109 +func WithHide(hide bool) MetadataOption {
110 + return func(m *Metadata) {
111 + m.Hide = hide
112 + }
113 +}
114 +
115 +type listener struct {
116 + mu sync.Mutex
117 +
118 + cred *cryptoops.Credential
119 + lease *rdverb.Lease
120 +
121 + conns map[*connection]struct{}
122 +
123 + connCh chan *connection
124 + closed bool
125 +}
126 +
127 +// Implement net.Listener interface for Listener
128 +func (l *listener) Accept() (net.Conn, error) {
129 + conn, ok := <-l.connCh
130 + if !ok {
131 + return nil, net.ErrClosed
132 + }
133 + return conn, nil
134 +}
135 +
136 +func (l *listener) Close() error {
137 + l.mu.Lock()
138 + defer l.mu.Unlock()
139 +
140 + if l.closed {
141 + return nil
142 + }
143 +
144 + l.closed = true
145 +
146 + // Close the connection channel first to prevent new connections
147 + close(l.connCh)
148 +
149 + // Close all active connections
150 + for conn := range l.conns {
151 + if err := conn.Close(); err != nil {
152 + log.Error().Err(err).Msg("[SDK] Error closing connection")
153 + }
154 + delete(l.conns, conn)
155 + }
156 +
157 + // Clear the connections map
158 + l.conns = make(map[*connection]struct{})
159 +
160 + return nil
161 +}
162 +
163 +func (l *listener) Addr() net.Addr {
164 + return addr(l.cred.ID())
165 +}
166 +
167 +type connRelay struct {
168 + addr string
169 + client *portal.RelayClient
170 + dialer func(context.Context, string) (io.ReadWriteCloser, error)
171 + stop chan struct{}
172 + stopOnce sync.Once // Ensure stop channel is closed only once
173 + mu sync.Mutex
174 +}
175 +
176 +var _ net.Conn = (*connection)(nil)
177 +
178 +type connection struct {
179 + via *connRelay
180 + localAddr string
181 + remoteAddr string
182 + conn *cryptoops.SecureConnection
183 +}
184 +
185 +func (r *connection) Read(b []byte) (n int, err error) {
186 + return r.conn.Read(b)
187 +}
188 +
189 +func (r *connection) Write(b []byte) (n int, err error) {
190 + return r.conn.Write(b)
191 +}
192 +
193 +func (r *connection) Close() error {
194 + return r.conn.Close()
195 +}
196 +
197 +func (r *connection) LocalAddr() net.Addr {
198 + return addr(r.localAddr)
199 +}
200 +
201 +func (r *connection) RemoteAddr() net.Addr {
202 + return addr(r.remoteAddr)
203 +}
204 +
205 +func (r *connection) SetDeadline(t time.Time) error {
206 + return r.conn.SetDeadline(t)
207 +}
208 +
209 +func (r *connection) SetReadDeadline(t time.Time) error {
210 + return r.conn.SetReadDeadline(t)
211 +}
212 +
213 +func (r *connection) SetWriteDeadline(t time.Time) error {
214 + return r.conn.SetWriteDeadline(t)
215 +}
216 +
217 +var _ net.Addr = (*addr)(nil)
218 +
219 +type addr string
220 +
221 +func (a addr) Network() string {
222 + return "portal"
223 +}
224 +
225 +func (a addr) String() string {
226 + return string(a)
227 +}