Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7ab89b1
add example config dir, logos, and update CONTRIBUTING.md
miloschwartz Jan 7, 2025
26a165a
update dockerignore
miloschwartz Jan 7, 2025
e1f0834
split base_url into dashboard_url and base_domain
miloschwartz Jan 8, 2025
b4dd827
Remove unessicary ports
oschwartz10612 Jan 8, 2025
ab69ded
Allow anything for the ip
oschwartz10612 Jan 8, 2025
fb754bc
Update docker tags
oschwartz10612 Jan 8, 2025
dc7bd41
Complex regex for domains/ips
oschwartz10612 Jan 8, 2025
b598fc3
update gitignore
miloschwartz Jan 8, 2025
184a22c
Merge branch 'main' into dev
miloschwartz Jan 8, 2025
a556339
allow hyphens in base_domain regex
miloschwartz Jan 9, 2025
6c81318
verify redirects are safe before redirecting
miloschwartz Jan 10, 2025
fcc6cad
hide create button if create org disable and bump version
miloschwartz Jan 10, 2025
c3d1945
allow resource redirect if host is same
miloschwartz Jan 10, 2025
c8c756d
Merge branch 'dev' of https://github.com/fosrl/pangolin into dev
oschwartz10612 Jan 11, 2025
832d7e5
Rename "IP Address" to "IP / Hostname"
oschwartz10612 Jan 11, 2025
484a099
Seperate ask for base domain and dashboard domain
oschwartz10612 Jan 11, 2025
38544cc
Add `site_block_size` and migration for beta.3
oschwartz10612 Jan 11, 2025
a853031
Constrict blocks and use CGNAT range for default
oschwartz10612 Jan 11, 2025
7b20329
change target form verbiage and update readme
miloschwartz Jan 11, 2025
82192fa
Merge branch 'dev' of https://github.com/fosrl/pangolin into dev
miloschwartz Jan 11, 2025
62ba797
Update installer to work with new domain split
oschwartz10612 Jan 11, 2025
a2bf3ba
router refresh on logout
miloschwartz Jan 11, 2025
e32301a
Merge branch 'dev' of https://github.com/fosrl/pangolin into dev
miloschwartz Jan 11, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ dist
.dist
installer
*.tar
bin
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
build-all:
build-release:
@if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make build-all tag=<tag>"; \
exit 1; \
Expand All @@ -12,6 +12,9 @@ build-arm:
build-x86:
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .

build-x86-ecr:
docker buildx build --platform linux/amd64 -t 216989133116.dkr.ecr.us-east-1.amazonaws.com/pangolin:latest --push .

build:
docker build -t fosrl/pangolin:latest .

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Pangolin

[![Documentation](https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square)](https://docs.fossorial.io/)
[![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin)
![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square)
[![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4)
[![Youtube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app)

Pangolin is a self-hosted tunneled reverse proxy management server with identity and access management, designed to securely expose private resources through use with the Traefik reverse proxy and WireGuard tunnel clients like Newt. With Pangolin, you retain full control over your infrastructure while providing a user-friendly and feature-rich solution for managing proxies, authentication, and access, and simplifying complex network setups, all with a clean and simple UI.

### Installation and Documentation
Expand Down
5 changes: 3 additions & 2 deletions config/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ traefik:
gerbil:
start_port: 51820
base_endpoint: localhost
block_size: 16
subnet_group: 10.0.0.0/8
block_size: 24
site_block_size: 30
subnet_group: 100.89.137.0/20
use_subdomain: true

rate_limits:
Expand Down
12 changes: 9 additions & 3 deletions install/Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@

all: build

build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o installer
build:
CGO_ENABLED=0 go build -o bin/installer

release:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/installer_linux_arm64

clean:
rm installer
rm bin/installer
rm bin/installer_linux_amd64
rm bin/installer_linux_arm64
11 changes: 6 additions & 5 deletions install/fs/config.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
app:
dashboard_url: https://{{.Domain}}
base_domain: {{.Domain}}
dashboard_url: https://{{.DashboardDomain}}
base_domain: {{.BaseDomain}}
log_level: info
save_logs: false

Expand All @@ -21,10 +21,11 @@ traefik:

gerbil:
start_port: 51820
base_endpoint: {{.Domain}}
base_endpoint: {{.DashboardDomain}}
use_subdomain: false
block_size: 16
subnet_group: 10.0.0.0/8
block_size: 24
site_block_size: 30
subnet_group: 100.89.137.0/20

rate_limits:
global:
Expand Down
8 changes: 4 additions & 4 deletions install/fs/traefik/dynamic_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ http:
routers:
# HTTP to HTTPS redirect router
main-app-router-redirect:
rule: "Host(`{{.Domain}}`)"
rule: "Host(`{{.DashboardDomain}}`)"
service: next-service
entryPoints:
- web
Expand All @@ -17,7 +17,7 @@ http:

# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "Host(`{{.Domain}}`) && !PathPrefix(`/api/v1`)"
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- websecure
Expand All @@ -26,7 +26,7 @@ http:

# API router (handles /api/v1 paths)
api-router:
rule: "Host(`{{.Domain}}`) && PathPrefix(`/api/v1`)"
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- websecure
Expand All @@ -35,7 +35,7 @@ http:

# WebSocket router
ws-router:
rule: "Host(`{{.Domain}}`)"
rule: "Host(`{{.DashboardDomain}}`)"
service: api-service
entryPoints:
- websecure
Expand Down
41 changes: 34 additions & 7 deletions install/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import (
var configFiles embed.FS

type Config struct {
Domain string `yaml:"domain"`
BaseDomain string `yaml:"baseDomain"`
DashboardDomain string `yaml:"dashboardUrl"`
LetsEncryptEmail string `yaml:"letsEncryptEmail"`
AdminUserEmail string `yaml:"adminUserEmail"`
AdminUserPassword string `yaml:"adminUserPassword"`
Expand All @@ -44,7 +45,10 @@ func main() {
// check if there is already a config file
if _, err := os.Stat("config/config.yml"); err != nil {
config := collectUserInput(reader)
createConfigFiles(config)
if err := createConfigFiles(config); err != nil {
fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1)
}

if !isDockerInstalled() && runtime.GOOS == "linux" {
if shouldInstallDocker() {
Expand Down Expand Up @@ -102,12 +106,13 @@ func collectUserInput(reader *bufio.Reader) Config {

// Basic configuration
fmt.Println("\n=== Basic Configuration ===")
config.Domain = readString(reader, "Enter your domain name", "")
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")

// Admin user configuration
fmt.Println("\n=== Admin User Configuration ===")
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.Domain)
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
for {
config.AdminUserPassword = readString(reader, "Enter admin user password", "")
if valid, message := validatePassword(config.AdminUserPassword); valid {
Expand Down Expand Up @@ -140,10 +145,14 @@ func collectUserInput(reader *bufio.Reader) Config {
}

// Validate required fields
if config.Domain == "" {
if config.BaseDomain == "" {
fmt.Println("Error: Domain name is required")
os.Exit(1)
}
if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required")
os.Exit(1)
}
if config.LetsEncryptEmail == "" {
fmt.Println("Error: Let's Encrypt email is required")
os.Exit(1)
Expand Down Expand Up @@ -269,8 +278,26 @@ func createConfigFiles(config Config) error {
return fmt.Errorf("error walking config files: %v", err)
}

// move the docker-compose.yml file to the root directory
os.Rename("config/docker-compose.yml", "docker-compose.yml")
// get the current directory
dir, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %v", err)
}

sourcePath := filepath.Join(dir, "config/docker-compose.yml")
destPath := filepath.Join(dir, "docker-compose.yml")

// Check if source file exists
if _, err := os.Stat(sourcePath); err != nil {
return fmt.Errorf("source docker-compose.yml not found: %v", err)
}

// Try to move the file
err = os.Rename(sourcePath, destPath)
if err != nil {
return fmt.Errorf("failed to move docker-compose.yml from %s to %s: %v",
sourcePath, destPath, err)
}

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fosrl/pangolin",
"version": "1.0.0-beta.2",
"version": "1.0.0-beta.3",
"private": true,
"type": "module",
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
Expand Down
3 changes: 2 additions & 1 deletion server/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ const environmentSchema = z.object({
base_endpoint: z.string().transform((url) => url.toLowerCase()),
use_subdomain: z.boolean(),
subnet_group: z.string(),
block_size: z.number().positive().gt(0)
block_size: z.number().positive().gt(0),
site_block_size: z.number().positive().gt(0)
}),
rate_limits: z.object({
global: z.object({
Expand Down
7 changes: 4 additions & 3 deletions server/routers/site/pickSiteDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { findNextAvailableCidr } from "@server/lib/ip";
import { generateId } from "@server/auth/sessions/app";
import config from "@server/lib/config";

export type PickSiteDefaultsResponse = {
exitNodeId: number;
Expand Down Expand Up @@ -51,9 +52,9 @@ export async function pickSiteDefaults(

// TODO: we need to lock this subnet for some time so someone else does not take it
let subnets = sitesQuery.map((site) => site.subnet);
// exclude the exit node address by replacing after the / with a /28
subnets.push(exitNode.address.replace(/\/\d+$/, "/29"));
const newSubnet = findNextAvailableCidr(subnets, 29, exitNode.address);
// exclude the exit node address by replacing after the / with a site block size
subnets.push(exitNode.address.replace(/\/\d+$/, `/${config.getRawConfig().gerbil.site_block_size}`));
const newSubnet = findNextAvailableCidr(subnets, config.getRawConfig().gerbil.site_block_size, exitNode.address);
if (!newSubnet) {
return next(
createHttpError(
Expand Down
10 changes: 10 additions & 0 deletions server/routers/user/acceptInvite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@ export async function acceptInvite(

const { user, session } = await verifySession(req);

// at this point we know the user exists
if (!user) {
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"You must be logged in to accept an invite"
)
);
}

if (user && user.email !== existingInvite.email) {
return next(
createHttpError(
Expand Down
4 changes: 3 additions & 1 deletion server/setup/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import { __DIRNAME } from "@server/lib/consts";
import { loadAppVersion } from "@server/lib/loadAppVersion";
import m1 from "./scripts/1.0.0-beta1";
import m2 from "./scripts/1.0.0-beta2";
import m3 from "./scripts/1.0.0-beta3";

// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA

// Define the migration list with versions and their corresponding functions
const migrations = [
{ version: "1.0.0-beta.1", run: m1 },
{ version: "1.0.0-beta.2", run: m2 }
{ version: "1.0.0-beta.2", run: m2 },
{ version: "1.0.0-beta.3", run: m3 }
// Add new migrations here as they are created
] as const;

Expand Down
42 changes: 42 additions & 0 deletions server/setup/scripts/1.0.0-beta3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import fs from "fs";
import yaml from "js-yaml";

export default async function migration() {
console.log("Running setup script 1.0.0-beta.3...");

// Determine which config file exists
const filePaths = [configFilePath1, configFilePath2];
let filePath = "";
for (const path of filePaths) {
if (fs.existsSync(path)) {
filePath = path;
break;
}
}

if (!filePath) {
throw new Error(
`No config file found (expected config.yml or config.yaml).`
);
}

// Read and parse the YAML file
let rawConfig: any;
const fileContents = fs.readFileSync(filePath, "utf8");
rawConfig = yaml.load(fileContents);

// Validate the structure
if (!rawConfig.gerbil) {
throw new Error(`Invalid config file: gerbil is missing.`);
}

// Update the config
rawConfig.gerbil.site_block_size = 29;

// Write the updated YAML back to the file
const updatedYaml = yaml.dump(rawConfig);
fs.writeFileSync(filePath, updatedYaml, "utf8");

console.log("Done.");
}
2 changes: 1 addition & 1 deletion src/app/[orgId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default async function OrgLayout(props: {
const user = await getUser();

if (!user) {
redirect(`/?redirect=/${orgId}`);
redirect(`/`);
}

try {
Expand Down
2 changes: 1 addition & 1 deletion src/app/[orgId]/settings/general/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default async function GeneralSettingsPage({
const user = await getUser();

if (!user) {
redirect(`/?redirect=/${orgId}/settings/general`);
redirect(`/`);
}

let orgUser = null;
Expand Down
2 changes: 1 addition & 1 deletion src/app/[orgId]/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
const user = await getUser();

if (!user) {
redirect(`/?redirect=/${params.orgId}/`);
redirect(`/`);
}

const cookie = await authCookieHeader();
Expand Down
Loading