Skip to content
Open
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
193 changes: 193 additions & 0 deletions .github/workflows/dev-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
name: Dev Build

on:
workflow_dispatch:
inputs:
version_suffix:
description: 'Version suffix (leave empty for auto: dev-YYYYMMDD-sha8)'
required: false
type: string

concurrency:
group: dev-build-${{ github.ref }}
cancel-in-progress: true

jobs:
build-binaries:
name: Build Binaries
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
matrix:
include:
- goos: linux
goarch: amd64
- goos: linux
goarch: arm64
- goos: darwin
goarch: amd64
- goos: darwin
goarch: arm64
- goos: windows
goarch: amd64
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Compute Version
id: version
run: |
SUFFIX="${{ inputs.version_suffix }}"
if [ -z "$SUFFIX" ]; then
# Auto-generate: dev-YYYYMMDD-sha8
VERSION="dev-$(date -u +'%Y%m%d')-${GITHUB_SHA::8}"
elif [[ "$SUFFIX" == dev-* ]]; then
# Already has dev- prefix
VERSION="$SUFFIX"
else
VERSION="dev-$SUFFIX"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Build WebUI
working-directory: ./webui
run: |
npm ci
npm run build

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.25.x'

- name: Build Go Binary
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
run: |
VERSION=${{ steps.version.outputs.version }}
GIT_COMMIT=${GITHUB_SHA::8}
BUILD_TIME=$(date -u +'%Y-%m-%dT%H:%M:%SZ')

OUTPUT_NAME=resin-${GOOS}-${GOARCH}
if [ "$GOOS" = "windows" ]; then
OUTPUT_NAME="${OUTPUT_NAME}.exe"
fi
echo "OUTPUT_NAME=${OUTPUT_NAME}" >> $GITHUB_ENV

mkdir -p build

go build -trimpath -tags "with_quic with_wireguard with_grpc with_utls with_embedded_tor with_naive_outbound" \
-ldflags="-s -w \
-X github.com/Resinat/Resin/internal/buildinfo.Version=${VERSION} \
-X github.com/Resinat/Resin/internal/buildinfo.GitCommit=${GIT_COMMIT} \
-X github.com/Resinat/Resin/internal/buildinfo.BuildTime=${BUILD_TIME}" \
-o build/${OUTPUT_NAME} ./cmd/resin

cd build

SIMPLE_NAME="resin"
if [ "$GOOS" = "windows" ]; then
SIMPLE_NAME="resin.exe"
fi
cp ${OUTPUT_NAME} ${SIMPLE_NAME}

if [ "$GOOS" = "windows" ]; then
zip resin-${GOOS}-${GOARCH}.zip ${SIMPLE_NAME}
PACKAGE_NAME="resin-${GOOS}-${GOARCH}.zip"
else
tar -czvf resin-${GOOS}-${GOARCH}.tar.gz ${SIMPLE_NAME}
PACKAGE_NAME="resin-${GOOS}-${GOARCH}.tar.gz"
fi

rm ${SIMPLE_NAME}
echo "PACKAGE_NAME=${PACKAGE_NAME}" >> $GITHUB_ENV

- name: Upload Release Package Artifact
uses: actions/upload-artifact@v4
with:
name: dev-release-${{ matrix.goos }}-${{ matrix.goarch }}
path: build/${{ env.PACKAGE_NAME }}
retention-days: 7

- name: Upload Linux bin for Docker
if: matrix.goos == 'linux'
uses: actions/upload-artifact@v4
with:
name: dev-binary-${{ matrix.goos }}-${{ matrix.goarch }}
path: build/${{ env.OUTPUT_NAME }}
retention-days: 1

docker:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: build-binaries
permissions:
contents: read
packages: write
steps:
- name: Checkout Code
uses: actions/checkout@v4

- name: Compute Version
id: version
run: |
SUFFIX="${{ inputs.version_suffix }}"
if [ -z "$SUFFIX" ]; then
VERSION="dev-$(date -u +'%Y%m%d')-${GITHUB_SHA::8}"
elif [[ "$SUFFIX" == dev-* ]]; then
VERSION="$SUFFIX"
else
VERSION="dev-$SUFFIX"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Download Linux amd64 binary
uses: actions/download-artifact@v4
with:
name: dev-binary-linux-amd64
path: release-bin/linux/amd64/

- name: Download Linux arm64 binary
uses: actions/download-artifact@v4
with:
name: dev-binary-linux-arm64
path: release-bin/linux/arm64/

- name: Give binaries execute permission
run: chmod +x release-bin/linux/amd64/resin-linux-amd64 release-bin/linux/arm64/resin-linux-arm64

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Lowercase repository name
run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./.github/Dockerfile.release
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ env.REPO_LC }}:${{ steps.version.outputs.version }}
ghcr.io/${{ env.REPO_LC }}:dev-latest
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
/resin
sing-box-reference
.agents
.ace-tool/
.claude/
.cursor/
.trellis/
AGENTS.md
data/
start.sh
.devcontainer
start-instance.sh
start-instance.sh
42 changes: 42 additions & 0 deletions internal/api/handler_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package api

import (
"net/http"
"time"

"github.com/Resinat/Resin/internal/service"
)

// HandleExportData returns a handler for GET /api/v1/data/export.
func HandleExportData(cp *service.ControlPlaneService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
payload, err := cp.ExportData()
if err != nil {
writeServiceError(w, err)
return
}

filename := "resin-export-" + time.Now().UTC().Format("20060102-150405") + ".json"
w.Header().Set("Content-Disposition", "attachment; filename=\""+filename+"\"")
WriteJSON(w, http.StatusOK, payload)
}
}

// HandleImportData returns a handler for POST /api/v1/data/import.
func HandleImportData(cp *service.ControlPlaneService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var payload service.ExportPayload
if err := DecodeBody(r, &payload); err != nil {
writeDecodeBodyError(w, err)
return
}

strategy := r.URL.Query().Get("strategy")
result, err := cp.ImportData(payload, strategy)
if err != nil {
writeServiceError(w, err)
return
}
WriteJSON(w, http.StatusOK, result)
}
}
37 changes: 36 additions & 1 deletion internal/api/handler_lease.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ func validateAccountPath(r *http.Request) (string, error) {

func leaseSortKey(sortBy string, l service.LeaseResponse) string {
switch sortBy {
case "node_tag":
return l.NodeTag
case "egress_ip":
return l.EgressIP
case "created_at":
return l.CreatedAt
case "expiry":
return l.Expiry
case "last_accessed":
Expand Down Expand Up @@ -92,7 +98,7 @@ func HandleListLeases(cp *service.ControlPlaneService) http.HandlerFunc {
leases = filtered
}

sorting, ok := parseSortingOrWriteInvalid(w, r, []string{"account", "expiry", "last_accessed"}, "expiry", "asc")
sorting, ok := parseSortingOrWriteInvalid(w, r, []string{"account", "node_tag", "egress_ip", "created_at", "expiry", "last_accessed"}, "expiry", "asc")
if !ok {
return
}
Expand Down Expand Up @@ -164,6 +170,35 @@ func HandleDeleteAllLeases(cp *service.ControlPlaneService) http.HandlerFunc {
}
}

// HandleBindLease returns a handler for PUT /api/v1/platforms/{id}/leases/{account}.
func HandleBindLease(cp *service.ControlPlaneService) http.HandlerFunc {
type bindRequest struct {
NodeHash string `json:"node_hash"`
}
return func(w http.ResponseWriter, r *http.Request) {
platformID, ok := requireUUIDPathParam(w, r, "id", "platform_id")
if !ok {
return
}
account, err := validateAccountPath(r)
if err != nil {
writeServiceError(w, err)
return
}
var req bindRequest
if err := DecodeBody(r, &req); err != nil {
writeDecodeBodyError(w, err)
return
}
lease, err := cp.BindLease(platformID, account, req.NodeHash)
if err != nil {
writeServiceError(w, err)
return
}
WriteJSON(w, http.StatusOK, lease)
}
}

// HandleIPLoad returns a handler for GET /api/v1/platforms/{id}/ip-load.
func HandleIPLoad(cp *service.ControlPlaneService) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
Expand Down
5 changes: 5 additions & 0 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func NewServerWithAddress(
authed.Handle("GET /api/v1/platforms/{id}/leases", HandleListLeases(cp))
authed.Handle("DELETE /api/v1/platforms/{id}/leases", HandleDeleteAllLeases(cp))
authed.Handle("GET /api/v1/platforms/{id}/leases/{account}", HandleGetLease(cp))
authed.Handle("PUT /api/v1/platforms/{id}/leases/{account}", HandleBindLease(cp))
authed.Handle("DELETE /api/v1/platforms/{id}/leases/{account}", HandleDeleteLease(cp))
authed.Handle("GET /api/v1/platforms/{id}/ip-load", HandleIPLoad(cp))

Expand Down Expand Up @@ -119,6 +120,10 @@ func NewServerWithAddress(
authed.Handle("GET /api/v1/geoip/lookup", HandleGeoIPLookup(cp))
authed.Handle("POST /api/v1/geoip/lookup", HandleGeoIPLookupPost(cp))
authed.Handle("POST /api/v1/geoip/actions/update-now", HandleGeoIPUpdate(cp))

// Data export / import.
authed.Handle("GET /api/v1/data/export", HandleExportData(cp))
authed.Handle("POST /api/v1/data/import", HandleImportData(cp))
}

// Request log endpoints (always registered if repo is available).
Expand Down
20 changes: 20 additions & 0 deletions internal/routing/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,16 @@ func (r *Router) tryLeaseHit(

newLease := current
newLease.LastAccessedNs = nowNs

// Auto-renew: extend lease when within 1 minute of expiry.
if newLease.ExpiryNs-nowNs < int64(time.Minute) {
ttl := plat.StickyTTLNs
if ttl <= 0 {
ttl = int64(24 * time.Hour)
}
newLease.ExpiryNs = nowNs + ttl
}

r.emitLeaseEvent(LeaseEvent{
Type: LeaseTouch,
PlatformID: plat.ID,
Expand Down Expand Up @@ -297,6 +307,16 @@ func (r *Router) tryLeaseSameIPRotation(
newLease := current
newLease.NodeHash = bestHash
newLease.LastAccessedNs = nowNs

// Auto-renew: extend lease when within 1 minute of expiry.
if newLease.ExpiryNs-nowNs < int64(time.Minute) {
ttl := plat.StickyTTLNs
if ttl <= 0 {
ttl = int64(24 * time.Hour)
}
newLease.ExpiryNs = nowNs + ttl
}

r.emitLeaseEvent(LeaseEvent{
Type: LeaseReplace,
PlatformID: plat.ID,
Expand Down
Loading