Commit b8ecf1e6 authored by nanahira's avatar nanahira

first

parent da5381b4
Pipeline #354 passed with stages
in 6 minutes and 14 seconds
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
/build
/output
.git*
.dockerignore
Dockerfile
.gitlab-ci.yml
/config.yaml
/ssl
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
/build
/output
/config.yaml
/ssl
stages:
- build
- deploy
variables:
GIT_DEPTH: "1"
CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
build:
stage: build
tags:
- docker
script:
- docker build --pull -t $CONTAINER_TEST_IMAGE .
- docker push $CONTAINER_TEST_IMAGE
deploy_latest:
stage: deploy
tags:
- docker
script:
- docker pull $CONTAINER_TEST_IMAGE
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_RELEASE_IMAGE
- docker push $CONTAINER_RELEASE_IMAGE
only:
- master
deploy_tag:
stage: deploy
tags:
- docker
variables:
CONTAINER_TAG_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
script:
- docker pull $CONTAINER_TEST_IMAGE
- docker tag $CONTAINER_TEST_IMAGE $CONTAINER_TAG_IMAGE
- docker push $CONTAINER_TAG_IMAGE
only:
- tags
FROM node:buster-slim
RUN apt update && apt -y install python3 && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
COPY ./package*.json ./
RUN npm ci
COPY . ./
RUN npm run build
CMD ["npm", "run", "start"]
This diff is collapsed.
address: 0.0.0.0
port: 443
ssl:
cert: ./ssl/fullchain.pem
key: ./ssl/privkey.pem
trustedProxies:
- 127.0.0.1
groups:
- name: Ladder Policy
description: Determine the routing of ladder.
allowedIPs:
- 0.0.0.0/0
sets:
- name: GFWList
description: Routing sites inside GFWList to ladder.
setname: u_gfwlist
allowedIPs:
- 0.0.0.0/0
- name: CHNRoute
description: Route all non-mainland sites to ladder.
setname: u_chnroute
allowedIPs:
- 0.0.0.0/0
- name: All
description: Route everything to ladder.
setname: u_all
allowedIPs:
- 0.0.0.0/0
import fs from "fs";
import yaml from "yaml";
export interface Group {
name: string,
description: string,
allowedIPs: string[],
sets: Set[]
}
export interface Set {
name: string,
description: string,
allowedIPs: string[],
setname: string,
here: boolean
}
export interface SSLOptions {
cert: string,
key: string
}
export interface Config {
address: string,
port: number,
ssl: SSLOptions,
trustedProxies: string[]
groups: Group[]
}
export async function loadConfig(): Promise<Config> {
const content: string = await fs.promises.readFile("./config.yaml", "utf-8");
const config: Config = yaml.parse(content);
return config;
}
import { ipset } from "netfilter";
import util from "util";
const ipset_create = util.promisify(ipset.create);
const ipset_add = util.promisify(ipset.add);
const ipset_del = util.promisify(ipset.del);
const ipset_test = util.promisify(ipset.test);
export class IPSetHelper {
static async create(setname: string): Promise<string> {
try {
await ipset_create({
setname,
type: "hash:net",
create_options: {
maxelem: 1000000
}
});
return null;
} catch(e) {
return e.toString();
}
}
static async add(setname: string, address: string): Promise<string> {
try {
await ipset_add({
setname,
entry: address
});
return null;
} catch(e) {
return e.toString();
}
}
static async del(setname: string, address: string): Promise<string> {
try {
await ipset_del({
setname,
entry: address
});
return null;
} catch(e) {
return e.toString();
}
}
static async test(setname: string, address: string): Promise<boolean> {
try {
await ipset_test({
setname,
entry: address
});
return true;
} catch(e) {
return false;
}
}
}
import ip from "ip";
import { Config } from "./config";
import { IPSetHelper } from "./ipset";
import _ from "underscore";
import bunyan from "bunyan";
function testRange(addr: string, range: string[]) {
if (!range) {
return true;
}
return _.any(range, r => {
return ip.cidrSubnet(r).contains(addr)
});
}
export interface ReturnMessage {
success: boolean,
message: string
}
export class Selector {
config: Config;
log: bunyan;
constructor(config: Config) {
this.config = config;
this.log = bunyan.createLogger({
name: "gateway-selector"
});
}
async init() {
for (let group of this.config.groups) {
for (let set of group.sets) {
if (!await IPSetHelper.create(set.setname)) {
this.log.info(`IPSet ${set.setname} created.`);
}
}
}
}
async get(addr: string) {
const groups = this.config.groups.filter(g => testRange(addr, g.allowedIPs)).map(g => _.clone(g));
for (let group of groups) {
const sets = group.sets.filter(s => testRange(addr, s.allowedIPs)).map(s => _.clone(s));
for (let set of sets) {
set.here = await IPSetHelper.test(set.setname, addr);
}
group.sets = sets;
}
return groups;
}
async add(addr: string, setname: string): Promise<ReturnMessage> {
const group = this.config.groups.find(g => g.sets.find(s => s.setname === setname));
if (!group) {
return { success: false, message: "Not found." };
}
if (!testRange(addr, group.allowedIPs)) {
return { success: false, message: "Forbidden." };
}
const set = group.sets.find(s => s.setname === setname);
if (!set) {
return { success: false, message: "Not found." };
}
if (!testRange(addr, set.allowedIPs)) {
return { success: false, message: "Forbidden." };
}
if (await IPSetHelper.test(set.setname, addr)) {
return { success: true, message: null };
}
const otherSets = group.sets.filter(s => s.setname !== setname);
for (let oset of otherSets) {
if (await IPSetHelper.test(oset.setname, addr)) {
await IPSetHelper.del(oset.setname, addr);
}
}
const message = await IPSetHelper.add(set.setname, addr);
this.log.info(`ADD: ${addr} ${set.name} => ${message}`);
return { success: !message, message };
}
async del(addr: string, setname: string): Promise<ReturnMessage> {
const group = this.config.groups.find(g => g.sets.find(s => s.setname === setname));
if (!group) {
return { success: false, message: "Not found." };
}
if (!testRange(addr, group.allowedIPs)) {
return { success: false, message: "Forbidden." };
}
const set = group.sets.find(s => s.setname === setname);
if (!set) {
return { success: false, message: "Not found." };
}
if (!testRange(addr, set.allowedIPs)) {
return { success: false, message: "Forbidden." };
}
if (!await IPSetHelper.test(set.setname, addr)) {
return { success: true, message: null };
}
const message = await IPSetHelper.del(set.setname, addr);
this.log.info(`DEL: ${addr} ${set.name} => ${message}`);
return { success: !message, message };
}
}
This diff is collapsed.
{
"name": "gateway-selector",
"version": "1.0.0",
"description": "Gateway web selector with IPSet",
"main": "build/server.js",
"dependencies": {
"@types/bunyan": "^1.8.6",
"@types/express": "^4.17.7",
"@types/ip": "^1.1.0",
"@types/node": "^14.0.26",
"@types/underscore": "^1.10.14",
"bunyan": "^1.8.14",
"express": "^4.17.1",
"ip": "^1.1.5",
"netfilter": "^0.3.3",
"typescript": "^3.9.7",
"underscore": "^1.10.2",
"yaml": "^1.10.0"
},
"devDependencies": {},
"scripts": {
"build": "./node_modules/.bin/tsc",
"start": "node build/server.js"
},
"repository": {
"type": "git",
"url": "git@git.mycard.moe:nanahira/gateway-selector.git"
},
"keywords": [
"ipset",
"gateway"
],
"author": "Nanahira",
"license": "AGPL-3.0"
}
import express from "express";
import { loadConfig } from "./lib/config";
import { Selector } from "./lib/selector";
import http from "http";
import https from "https";
import fs from "fs";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
async function main() {
const config = await loadConfig();
const selector = new Selector(config);
await selector.init();
app.set("trust proxy", config.trustedProxies);
app.get("/api/get", async (req: express.Request, res: express.Response) => {
const addr = req.ip;
const groups = await selector.get(addr);
res.json({ip: addr, groups});
});
app.post("/api/add", async (req: express.Request, res: express.Response) => {
const addr = req.ip;
const setname = req.body.setname;
const result = await selector.add(addr, setname);
res.json(result);
});
app.post("/api/del", async (req: express.Request, res: express.Response) => {
const addr = req.ip;
const setname = req.body.setname;
const result = await selector.del(addr, setname);
res.json(result);
});
if (config.ssl) {
const cert = await fs.promises.readFile(config.ssl.cert);
const key = await fs.promises.readFile(config.ssl.key);
const server = https.createServer({cert, key}, app);
server.listen(config.port, config.address);
} else {
const server = http.createServer(app);
server.listen(config.port, config.address);
}
}
main();
{
"compilerOptions": {
"outDir": "build",
"module": "commonjs",
"target": "esnext",
"esModuleInterop": true,
"sourceMap": true
},
"compileOnSave": true,
"allowJs": true,
"include": [
"*.ts"
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment