├── sdk
├── rust
│ ├── src
│ │ ├── lib.rs
│ │ └── fbs
│ │ │ └── mod.rs
│ └── Cargo.toml
└── go
│ ├── go.mod
│ ├── go.sum
│ ├── sdk
│ ├── logging.go
│ ├── response_body_filter.go
│ ├── request_filter.go
│ ├── response_filter.go
│ ├── types.go
│ └── constants.go
│ └── fbs
│ └── nylon_plugin
│ ├── RemoveResponseHeader.go
│ ├── HeaderKeyValue.go
│ └── NylonHttpHeaders.go
├── crates
├── nylon-plugin
│ ├── src
│ │ ├── native
│ │ │ ├── mod.rs
│ │ │ └── header_modifier.rs
│ │ ├── types.rs
│ │ ├── plugin_manager.rs
│ │ ├── constants.rs
│ │ ├── loaders.rs
│ │ └── stream.rs
│ └── Cargo.toml
├── nylon-config
│ ├── src
│ │ ├── lib.rs
│ │ ├── utils.rs
│ │ ├── services.rs
│ │ └── runtime.rs
│ └── Cargo.toml
├── nylon-error
│ ├── Cargo.toml
│ └── src
│ │ └── lib.rs
├── nylon-command
│ ├── Cargo.toml
│ └── src
│ │ ├── lib.rs
│ │ └── service.rs
├── nylon-tls
│ ├── src
│ │ ├── lib.rs
│ │ ├── certificate.rs
│ │ └── metrics.rs
│ └── Cargo.toml
├── nylon-types
│ ├── src
│ │ ├── lib.rs
│ │ ├── proxy.rs
│ │ ├── tls.rs
│ │ ├── route.rs
│ │ ├── services.rs
│ │ ├── plugins.rs
│ │ ├── websocket.rs
│ │ └── context.rs
│ └── Cargo.toml
├── nylon-store
│ ├── Cargo.toml
│ └── src
│ │ ├── lib.rs
│ │ ├── tls.rs
│ │ └── websockets.rs
└── nylon
│ ├── Cargo.toml
│ └── src
│ ├── backend.rs
│ ├── dynamic_certificate.rs
│ ├── response.rs
│ ├── context.rs
│ └── runtime.rs
├── docs
├── .gitignore
├── .vitepress
│ ├── theme
│ │ ├── index.ts
│ │ └── custom.css
│ └── config.ts
├── package.json
├── README.md
├── public
│ ├── logo.svg
│ └── install
├── introduction
│ ├── what-is-nylon.md
│ ├── quick-start.md
│ └── installation.md
├── examples
│ └── basic-proxy.md
├── plugins
│ └── go-sdk.md
└── core
│ └── routing.md
├── .gitignore
├── examples
├── go
│ ├── go.sum
│ ├── go.mod
│ └── main.go
├── static
│ └── index.html
├── config.yaml
├── proxy
│ ├── host_route.yaml
│ ├── tls.yaml
│ └── base.yaml
└── cert
│ ├── localhost.crt
│ └── localhost.key
├── proto
├── plugin.fbs
├── dispatcher.fbs
└── http_context.fbs
├── c
└── nylon.h
├── .github
├── FUNDING.yml
└── workflows
│ └── Ci.yml
├── LICENSE
├── Cargo.toml
├── README.md
├── Makefile
└── scripts
└── install.sh
/sdk/rust/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod fbs;
2 |
--------------------------------------------------------------------------------
/crates/nylon-plugin/src/native/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod header_modifier;
2 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .vitepress/dist
3 | .vitepress/cache
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | Cargo.lock
2 | **/*.rs.bk
3 | *.pdb
4 | /target
5 | .config
6 | .DS_Store
--------------------------------------------------------------------------------
/crates/nylon-config/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod proxy;
2 | pub mod runtime;
3 | pub mod services;
4 | mod utils;
5 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/index.ts:
--------------------------------------------------------------------------------
1 | import DefaultTheme from 'vitepress/theme'
2 | import './custom.css'
3 |
4 | export default DefaultTheme
5 |
6 |
--------------------------------------------------------------------------------
/sdk/go/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/AssetsArt/nylon/sdk/go
2 |
3 | go 1.24.3
4 |
5 | require github.com/google/flatbuffers v25.2.10+incompatible
6 |
--------------------------------------------------------------------------------
/sdk/rust/src/fbs/mod.rs:
--------------------------------------------------------------------------------
1 | #![allow(unused)]
2 | #![allow(clippy::all, clippy::pedantic)]
3 | #![allow(unsafe_op_in_unsafe_fn)]
4 | pub mod plugin_generated;
5 |
--------------------------------------------------------------------------------
/crates/nylon-error/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nylon-error"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | thiserror = { workspace = true }
8 | serde_json = { workspace = true }
--------------------------------------------------------------------------------
/sdk/go/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
2 | github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
3 |
--------------------------------------------------------------------------------
/examples/go/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
2 | github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
3 |
--------------------------------------------------------------------------------
/proto/plugin.fbs:
--------------------------------------------------------------------------------
1 | namespace nylon_plugin;
2 |
3 | table HeaderKeyValue {
4 | key: string (required);
5 | value: string (required);
6 | }
7 |
8 | table NylonHttpHeaders {
9 | headers: [HeaderKeyValue] (required);
10 | }
11 |
--------------------------------------------------------------------------------
/crates/nylon-command/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nylon-command"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | clap = { workspace = true }
8 | service-manager = { workspace = true }
9 | thiserror = { workspace = true }
10 | tracing = { workspace = true }
--------------------------------------------------------------------------------
/crates/nylon-tls/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::type_complexity)]
2 | pub mod acme;
3 | pub mod certificate;
4 | pub mod metrics;
5 |
6 | pub use acme::AcmeClient;
7 | pub use certificate::{CertificateInfo, CertificateStore};
8 | pub use metrics::{AcmeMetrics, DomainMetrics, MetricsSummary};
9 |
--------------------------------------------------------------------------------
/examples/go/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/AssetsArt/nylon/examples/go
2 |
3 | go 1.24.3
4 |
5 | require github.com/AssetsArt/nylon/sdk/go v0.0.0
6 |
7 | require github.com/google/flatbuffers v25.2.10+incompatible // indirect
8 |
9 | replace github.com/AssetsArt/nylon/sdk/go => ../../sdk/go
10 |
--------------------------------------------------------------------------------
/crates/nylon-types/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod context;
2 | pub mod plugins;
3 | pub mod proxy;
4 | pub mod route;
5 | pub mod services;
6 | pub mod template;
7 | pub mod tls;
8 | pub mod websocket;
9 |
10 | /// Nylon runtime server instance
11 | #[derive(Debug, Clone)]
12 | pub struct NylonRuntime {}
13 |
--------------------------------------------------------------------------------
/proto/dispatcher.fbs:
--------------------------------------------------------------------------------
1 | namespace nylon_dispatcher;
2 |
3 | table NylonDispatcher {
4 | http_end: bool;
5 | request_id: string (required);
6 | name: string;
7 | entry: string;
8 | data: [ubyte] (required);
9 | payload: [ubyte];
10 | store: [ubyte];
11 | }
12 |
13 | root_type NylonDispatcher;
14 |
--------------------------------------------------------------------------------
/sdk/rust/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nylon-sdk"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | nylon-types = { path = "../../crates/nylon-types" }
8 | nylon-error = { path = "../../crates/nylon-error" }
9 | flatbuffers = { workspace = true }
10 | pingora = { workspace = true }
11 | bytes = { workspace = true }
--------------------------------------------------------------------------------
/examples/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Nylon Static
7 |
8 |
9 | Nylon Demo
10 | This page is served by Nylon's static service.
11 |
12 |
--------------------------------------------------------------------------------
/sdk/go/sdk/logging.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | func (p *PhaseLogging) Response() *Response {
4 | return &Response{
5 | ctx: p.ctx,
6 | }
7 | }
8 |
9 | func (p *PhaseLogging) Request() *Request {
10 | return &Request{
11 | ctx: p.ctx,
12 | }
13 | }
14 |
15 | func (p *PhaseLogging) GetPayload() map[string]any {
16 | return p.ctx.GetPayload()
17 | }
18 |
19 | func (p *PhaseLogging) Next() {
20 | p.ctx.Next()
21 | }
22 |
--------------------------------------------------------------------------------
/sdk/go/sdk/response_body_filter.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | func (p *PhaseResponseBodyFilter) Response() *Response {
4 | return &Response{
5 | ctx: p.ctx,
6 | }
7 | }
8 |
9 | func (p *PhaseResponseBodyFilter) Request() *Request {
10 | return &Request{
11 | ctx: p.ctx,
12 | }
13 | }
14 |
15 | func (p *PhaseResponseBodyFilter) GetPayload() map[string]any {
16 | return p.ctx.GetPayload()
17 | }
18 |
19 | func (p *PhaseResponseBodyFilter) Next() {
20 | p.ctx.Next()
21 | }
22 |
--------------------------------------------------------------------------------
/crates/nylon-config/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nylon-config"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | nylon-error = { path = "../nylon-error" }
8 | nylon-store = { path = "../nylon-store" }
9 | nylon-types = { path = "../nylon-types" }
10 | nylon-plugin = { path = "../nylon-plugin" }
11 | serde_yaml_ng = { workspace = true }
12 | serde = { workspace = true }
13 | serde_json = { workspace = true }
14 | num_cpus = { workspace = true }
15 | async-trait = { workspace = true }
--------------------------------------------------------------------------------
/c/nylon.h:
--------------------------------------------------------------------------------
1 | #ifndef NYLON_H
2 | #define NYLON_H
3 |
4 | #include
5 | #include
6 | #include
7 |
8 | typedef struct {
9 | uint32_t sid;
10 | uint8_t phase;
11 | uint32_t method;
12 | const unsigned char *ptr;
13 | uint64_t len;
14 | } FfiBuffer;
15 |
16 | typedef void (*data_event_fn)(const FfiBuffer* ffiBuffer);
17 |
18 | static inline void call_event_method(data_event_fn cb, const FfiBuffer* ffiBuffer) {
19 | cb(ffiBuffer);
20 | }
21 |
22 | #endif // NYLON_H
--------------------------------------------------------------------------------
/crates/nylon-types/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nylon-types"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | nylon-error = { path = "../nylon-error" }
8 | serde = { workspace = true }
9 | serde_json = { workspace = true }
10 | pingora = { workspace = true }
11 | regex = { workspace = true }
12 | chrono = { workspace = true }
13 | uuid = { workspace = true }
14 | bytes = { workspace = true }
15 | libloading = { workspace = true }
16 | async-trait = { workspace = true }
17 | tokio = { workspace = true }
18 | lru = { workspace = true }
19 | once_cell = { workspace = true }
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nylon-docs",
3 | "version": "1.0.0",
4 | "type": "module",
5 | "description": "Documentation for Nylon Reverse Proxy",
6 | "scripts": {
7 | "dev": "vitepress dev",
8 | "build": "npm run copy-install && vitepress build",
9 | "preview": "vitepress preview",
10 | "copy-install": "cp ../scripts/install.sh public/install"
11 | },
12 | "keywords": ["nylon", "proxy", "reverse-proxy", "documentation"],
13 | "author": "",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "vitepress": "^1.0.0",
17 | "vue": "^3.4.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/crates/nylon-tls/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nylon-tls"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | instant-acme = { workspace = true }
8 | tokio = { workspace = true, features = ["full"] }
9 | tracing = { workspace = true }
10 | thiserror = { workspace = true }
11 | openssl = { workspace = true }
12 | serde = { workspace = true }
13 | serde_json = { workspace = true }
14 | chrono = { workspace = true, features = ["serde"] }
15 | rcgen = { workspace = true }
16 | dashmap = { workspace = true }
17 | nylon-types = { path = "../nylon-types" }
18 | nylon-error = { path = "../nylon-error" }
19 |
--------------------------------------------------------------------------------
/proto/http_context.fbs:
--------------------------------------------------------------------------------
1 | namespace nylon_http_context;
2 |
3 | table KeyValue {
4 | key: string (required);
5 | value: string (required);
6 | }
7 |
8 | table NylonHttpRequest {
9 | method: string (required);
10 | path: string (required);
11 | query: string;
12 | params: [KeyValue];
13 | headers: [KeyValue];
14 | body: [ubyte];
15 | }
16 |
17 | table NylonHttpResponse {
18 | status: int;
19 | headers: [KeyValue];
20 | body: [ubyte];
21 | }
22 |
23 | table NylonHttpContext {
24 | request: NylonHttpRequest (required);
25 | response: NylonHttpResponse (required);
26 | }
27 |
28 | root_type NylonHttpContext;
29 |
--------------------------------------------------------------------------------
/crates/nylon-types/src/proxy.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | plugins::PluginItem,
3 | route::{MiddlewareItem, RouteConfig},
4 | services::ServiceItem,
5 | tls::TlsConfig,
6 | };
7 | use serde::Deserialize;
8 | use std::collections::HashMap;
9 |
10 | #[derive(Debug, Deserialize, Clone, Default)]
11 | pub struct ProxyConfig {
12 | pub services: Option>,
13 | pub tls: Option>,
14 | pub header_selector: Option,
15 | pub routes: Option>,
16 | pub plugins: Option>,
17 | pub middleware_groups: Option>>,
18 | }
19 |
--------------------------------------------------------------------------------
/examples/config.yaml:
--------------------------------------------------------------------------------
1 | http:
2 | - 0.0.0.0:8088
3 | https:
4 | - 0.0.0.0:8443
5 | metrics:
6 | - 127.0.0.1:6192
7 | config_dir: "./examples/proxy"
8 | acme: "./examples/acme"
9 | pingora:
10 | # https://github.com/cloudflare/pingora/blob/main/docs/user_guide/daemon.md
11 | daemon: false
12 | # https://github.com/cloudflare/pingora/blob/main/docs/user_guide/conf.md
13 | grace_period_seconds: 1
14 | graceful_shutdown_timeout_seconds: 1
15 |
16 | # WebSocket adapter configuration (optional)
17 | # websocket:
18 | # adapter_type: redis # memory | redis | cluster
19 | # redis:
20 | # host: "localhost"
21 | # port: 6379
22 | # password: null
23 | # db: 0
24 | # key_prefix: "nylon:ws"
--------------------------------------------------------------------------------
/crates/nylon-store/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nylon-store"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | nylon-types = { path = "../nylon-types" }
8 | nylon-error = { path = "../nylon-error" }
9 | nylon-tls = { path = "../nylon-tls" }
10 | dashmap = { workspace = true }
11 | once_cell = { workspace = true }
12 | pingora = { workspace = true }
13 | fnv = { workspace = true }
14 | lru = { workspace = true }
15 | matchit = { workspace = true }
16 | tracing = { workspace = true }
17 | serde_json = { workspace = true }
18 | async-trait = { workspace = true }
19 | tokio = { workspace = true }
20 | tokio-stream = { workspace = true }
21 | uuid = { workspace = true }
22 | chrono = { workspace = true }
23 | redis = { workspace = true }
--------------------------------------------------------------------------------
/crates/nylon-plugin/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nylon-plugin"
3 | version = "0.1.0"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | nylon-types = { path = "../nylon-types" }
8 | nylon-error = { path = "../nylon-error" }
9 | nylon-store = { path = "../nylon-store" }
10 | nylon-sdk = { path = "../../sdk/rust" }
11 | serde_json = { workspace = true }
12 | serde = { workspace = true }
13 | pingora = { workspace = true }
14 | tracing = { workspace = true }
15 | libloading = { workspace = true }
16 | dashmap = { workspace = true }
17 | flatbuffers = { workspace = true }
18 | bytes = { workspace = true }
19 | tokio = { workspace = true }
20 | once_cell = { workspace = true }
21 | async-trait = { workspace = true }
22 | http = { workspace = true }
23 | sha1 = { workspace = true }
24 | base64 = { workspace = true }
25 | chrono = { workspace = true }
--------------------------------------------------------------------------------
/crates/nylon/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "nylon"
3 | version = "1.0.0-beta.4"
4 | edition = "2024"
5 |
6 | [dependencies]
7 | nylon-command = { path = "../nylon-command" }
8 | nylon-config = { path = "../nylon-config" }
9 | nylon-error = { path = "../nylon-error" }
10 | nylon-types = { path = "../nylon-types" }
11 | nylon-store = { path = "../nylon-store" }
12 | nylon-plugin = { path = "../nylon-plugin" }
13 | nylon-tls = { path = "../nylon-tls" }
14 | nylon-sdk = { path = "../../sdk/rust" }
15 | tracing-subscriber = { workspace = true }
16 | tracing = { workspace = true }
17 | pingora = { workspace = true }
18 | async-trait = { workspace = true }
19 | openssl = { workspace = true }
20 | tokio = { workspace = true }
21 | bytes = { workspace = true }
22 | serde_json = { workspace = true }
23 | http = { workspace = true }
24 | flatbuffers = { workspace = true }
25 | dashmap = { workspace = true }
26 | mime_guess = { workspace = true }
27 | fastrand = { workspace = true }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [AssetsArt] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/examples/proxy/host_route.yaml:
--------------------------------------------------------------------------------
1 |
2 | # https://github.com/ibraheemdev/matchit
3 |
4 | # host & path routing
5 | routes:
6 | # App
7 | - route:
8 | type: host
9 | value: localhost # localhost|api.localhost
10 | name: app-route
11 | middleware:
12 | - group: auth
13 | paths:
14 | - path:
15 | - /static
16 | - /static/{*path}
17 | service:
18 | name: static
19 | rewrite: /static
20 | - path:
21 | - /ws
22 | methods: [GET, POST, OPTIONS]
23 | service:
24 | name: ws-service
25 | - path:
26 | - /myapp
27 | - /myapp/{name}
28 | service:
29 | name: myapp-service
30 | - path:
31 | - /stream
32 | methods: [GET, POST, OPTIONS]
33 | service:
34 | name: stream-service
35 | - path:
36 | - /
37 | - /{*path}
38 | middleware:
39 | - group: security
40 | service:
41 | name: app-service
--------------------------------------------------------------------------------
/crates/nylon-config/src/utils.rs:
--------------------------------------------------------------------------------
1 | use nylon_error::NylonError;
2 | use std::path::PathBuf;
3 |
4 | pub fn read_dir_recursive(dir: &String, max_depth: u16) -> Result, NylonError> {
5 | let mut files = Vec::new();
6 | let path_buf = PathBuf::from(dir);
7 | for entry in std::fs::read_dir(path_buf).map_err(|e| {
8 | NylonError::ConfigError(format!("Unable to read config directory {:?}: {}", dir, e))
9 | })? {
10 | let entry = entry.map_err(|e| {
11 | NylonError::ConfigError(format!(
12 | "Unable to read file in config directory {:?}: {}",
13 | dir, e
14 | ))
15 | })?;
16 | let path = entry.path();
17 | if path.is_dir() {
18 | if max_depth > 0 {
19 | files.append(&mut read_dir_recursive(
20 | &path.to_string_lossy().to_string(),
21 | max_depth - 1,
22 | )?);
23 | }
24 | } else {
25 | files.push(path);
26 | }
27 | }
28 | Ok(files)
29 | }
30 |
--------------------------------------------------------------------------------
/sdk/go/sdk/request_filter.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | func (ctx *PhaseRequestFilter) Request() *Request {
4 | return &Request{
5 | ctx: ctx.ctx,
6 | }
7 | }
8 |
9 | func (ctx *PhaseRequestFilter) Response() *Response {
10 | return &Response{
11 | ctx: ctx.ctx,
12 | }
13 | }
14 |
15 | func (p *PhaseRequestFilter) GetPayload() map[string]any {
16 | return p.ctx.GetPayload()
17 | }
18 |
19 | func (p *PhaseRequestFilter) Next() {
20 | p.ctx.Next()
21 | }
22 |
23 | func (p *PhaseRequestFilter) End() {
24 | p.ctx.End()
25 | }
26 |
27 | // WebSocket helpers
28 | func (p *PhaseRequestFilter) WebSocketUpgrade(cbs WebSocketCallbacks) error {
29 | // Store callbacks in context for dispatch before requesting upgrade
30 | // This ensures callbacks are available when events arrive
31 | p.ctx.mu.Lock()
32 | p.ctx.wsCallbacks = &cbs
33 | p.ctx.wsUpgraded = false // Reset state before upgrade
34 | p.ctx.mu.Unlock()
35 |
36 | // Ask Rust to upgrade - this will trigger OnOpen event after handshake
37 | return RequestMethod(p.ctx.sessionID, 0, NylonMethodWebSocketUpgrade, nil)
38 | }
39 |
--------------------------------------------------------------------------------
/crates/nylon-command/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod handler;
2 | mod service;
3 |
4 | use clap::{Parser, Subcommand};
5 |
6 | pub use handler::{ServiceError, handle_service_command};
7 | pub use service::ServiceCommands;
8 |
9 | #[derive(Parser, Debug)]
10 | #[command(version, about, long_about = None)]
11 | pub struct Cli {
12 | #[command(subcommand)]
13 | pub command: Commands,
14 | }
15 |
16 | #[derive(Debug, Subcommand)]
17 | pub enum Commands {
18 | #[command(name = "service", short_flag = 's')]
19 | #[command(about = "Manage the proxy daemon service (install, start, stop, etc.)")]
20 | #[command(subcommand)]
21 | Service(ServiceCommands),
22 |
23 | // run with no command
24 | #[command(name = "run")]
25 | #[command(about = "Run the proxy server with a config file")]
26 | Run {
27 | #[arg(long, short = 'c', default_value = "/etc/nylon/config.yaml")]
28 | #[arg(help = "Path to the config file example: /etc/nylon/config.yaml")]
29 | config: String,
30 | },
31 | }
32 |
33 | pub fn parse() -> Cli {
34 | Cli::parse()
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 AssetsArt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/crates/nylon/src/backend.rs:
--------------------------------------------------------------------------------
1 | use nylon_error::NylonError;
2 | use nylon_store::lb_backends::{BackendType, HttpService};
3 | use nylon_types::context::NylonContext;
4 | use pingora::{lb::Backend, proxy::Session};
5 |
6 | pub fn selection(
7 | service: &HttpService,
8 | session: &mut Session,
9 | ctx: &mut NylonContext,
10 | ) -> Result {
11 | let mut selection_key = ctx.client_ip.read().expect("lock").clone();
12 | if let Some(header_value) = session.req_header().headers.get("x-forwarded-for") {
13 | let value = header_value.to_str().unwrap_or_default();
14 | selection_key.push_str(value);
15 | }
16 | match &service.backend_type {
17 | BackendType::RoundRobin(lb) => lb.select(selection_key.as_bytes(), 256),
18 | BackendType::Weighted(lb) => lb.select(selection_key.as_bytes(), 256),
19 | BackendType::Consistent(lb) => lb.select(selection_key.as_bytes(), 256),
20 | BackendType::Random(lb) => lb.select(selection_key.as_bytes(), 256),
21 | }
22 | .ok_or(NylonError::HttpException(
23 | 500,
24 | "INTERNAL_SERVER_ERROR",
25 | "No backend found",
26 | ))
27 | }
28 |
--------------------------------------------------------------------------------
/examples/cert/localhost.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDDzCCAfegAwIBAgIUNr7z1bLoetY9ZXYtiF/hfZQ9K9owDQYJKoZIhvcNAQEL
3 | BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MDYxNDA1NTQyOFoXDTI1MDcx
4 | NDA1NTQyOFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
5 | AAOCAQ8AMIIBCgKCAQEA2QZEci0Vp/symjYz9wGIvDz1CXpsr125c/lVCo4jEpaf
6 | pQajXMTDGOnWQr4R/RHzhFarZsb6kL/ot0luqSj1L/tp7The8u5rZZNTu+N+xeWK
7 | RVsIEtplkqzFSE83fLXezKtBGkU3rG/GHC3+KJVz4hY5IqvcP7yLV/S7SX97twNk
8 | strMueeG10x3FHxtA5weF1eqslD6Iu6t4VMgfMvK4Jx1Jphyx37FlP5ER7cy7uh1
9 | lD3bNAwotm4jVKXYCAMeCjpOHHqVoOOSOwdGZFM+fEx6TakiXCBCUtRZxXCG4N5T
10 | JDdoYj+L3PeI5YBKfv0npu2j/icngEqnN6K6Muk1YQIDAQABo1kwVzAUBgNVHREE
11 | DTALgglsb2NhbGhvc3QwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB
12 | MB0GA1UdDgQWBBRCm2GHqEaqrZnndTpzJnHC9NMwsTANBgkqhkiG9w0BAQsFAAOC
13 | AQEAXf/YDLqzm21l8BzjGBHXVzR0cf3qajxUcpD3y20tCNBtr+ijLnCPjKGrAfr5
14 | 9ApB74ZijCk90wvwNsxtgNdFadBmdM9epZRrNkqzEn65+B4SVuD35kgos/I9dS1E
15 | 14qAeBUZYwcSz4XekZfR6/cPHrsuvQGrRu5E2bv1p1oSaE1DAzuXav0dDabMoDC8
16 | p39MXumqetIHHpqyWo4UUsiOvyFQdModviNW77up1PYjWxHPmUqKnHf/13N74HHa
17 | CRmmRQcErbWcRPxI8BBPrX0UgRHzyeZaPDfVa7he8SV0696Is/S69yhH/sJ80A0n
18 | GTIaeFOA9dp4k1FLGKRnyxGLTA==
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/crates/nylon-command/src/service.rs:
--------------------------------------------------------------------------------
1 | use clap::Subcommand;
2 |
3 | #[derive(Debug, Subcommand)]
4 | pub enum ServiceCommands {
5 | // Install the service
6 | #[command(name = "install")]
7 | #[command(about = "Install the service.")]
8 | Install,
9 |
10 | // Uninstall the service
11 | #[command(name = "uninstall")]
12 | #[command(about = "Uninstall the service.")]
13 | Uninstall,
14 |
15 | // Start the service
16 | #[command(name = "start")]
17 | #[command(about = "Start the service and apply all configuration changes.")]
18 | Start,
19 |
20 | // Stop the service
21 | #[command(name = "stop")]
22 | #[command(about = "Stop the service.")]
23 | Stop,
24 |
25 | // Restart the service
26 | #[command(name = "restart")]
27 | #[command(about = "Restart the service and apply all configuration changes.")]
28 | Restart,
29 |
30 | // Status of the service
31 | #[command(name = "status")]
32 | #[command(about = "Show the status of the service.")]
33 | Status,
34 |
35 | // Reload the service
36 | #[command(name = "reload")]
37 | #[command(
38 | about = "Reload route and service configurations without applying global configuration changes."
39 | )]
40 | Reload,
41 | }
42 |
--------------------------------------------------------------------------------
/sdk/go/sdk/response_filter.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | func (p *PhaseResponseFilter) Response() *Response {
4 | return &Response{
5 | ctx: p.ctx,
6 | }
7 | }
8 |
9 | func (p *PhaseResponseFilter) Request() *Request {
10 | return &Request{
11 | ctx: p.ctx,
12 | }
13 | }
14 |
15 | func (p *PhaseResponseFilter) SetResponseHeader(key, value string) {
16 | httpCtx := Response{
17 | ctx: p.ctx,
18 | }
19 | httpCtx.SetHeader(key, value)
20 | }
21 |
22 | func (p *PhaseResponseFilter) RemoveResponseHeader(key string) {
23 | httpCtx := Response{
24 | ctx: p.ctx,
25 | }
26 | httpCtx.RemoveHeader(key)
27 | }
28 |
29 | func (p *PhaseResponseFilter) SetResponseStatus(status uint16) {
30 | httpCtx := Response{
31 | ctx: p.ctx,
32 | }
33 | httpCtx.SetStatus(status)
34 | }
35 |
36 | func (p *PhaseResponseFilter) GetRequestHeader(key string) string {
37 | httpCtx := Request{
38 | ctx: p.ctx,
39 | }
40 | return httpCtx.Headers().Get(key)
41 | }
42 |
43 | func (p *PhaseResponseFilter) GetRequestHeaders() map[string]string {
44 | httpCtx := Request{
45 | ctx: p.ctx,
46 | }
47 | return httpCtx.Headers().GetAll()
48 | }
49 |
50 | func (p *PhaseResponseFilter) GetPayload() map[string]any {
51 | return p.ctx.GetPayload()
52 | }
53 |
54 | func (p *PhaseResponseFilter) Next() {
55 | p.ctx.Next()
56 | }
57 |
--------------------------------------------------------------------------------
/crates/nylon-plugin/src/types.rs:
--------------------------------------------------------------------------------
1 | //! Types and structures used throughout the plugin system
2 |
3 | use nylon_types::{route::MiddlewareItem, template::Expr};
4 | use serde_json::Value;
5 | use std::collections::HashMap;
6 |
7 | /// Built-in plugins that are available by default
8 | #[derive(Debug, Clone, PartialEq)]
9 | pub enum BuiltinPlugin {
10 | RequestHeaderModifier,
11 | ResponseHeaderModifier,
12 | }
13 |
14 | /// Context for middleware execution
15 | #[derive(Debug, Clone)]
16 | pub struct MiddlewareContext {
17 | pub middleware: MiddlewareItem,
18 | pub payload: Option,
19 | pub payload_ast: Option>>,
20 | }
21 |
22 | /// Result of plugin execution
23 | #[derive(Debug, Clone, Default)]
24 | pub struct PluginResult {
25 | pub http_end: bool,
26 | pub stream_end: bool,
27 | }
28 |
29 | impl PluginResult {
30 | /// Create a new plugin result
31 | pub fn new(http_end: bool, stream_end: bool) -> Self {
32 | Self {
33 | http_end,
34 | stream_end,
35 | }
36 | }
37 | }
38 |
39 | /// Plugin execution context
40 | #[derive(Debug)]
41 | pub struct PluginExecutionContext<'a> {
42 | pub plugin_name: &'a str,
43 | pub entry: &'a str,
44 | pub payload: &'a Option>,
45 | pub params: &'a Option>,
46 | }
47 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "3"
3 |
4 | members = [
5 | "crates/*", "sdk/rust",
6 | ]
7 |
8 | [workspace.dependencies]
9 | thiserror = "2.0"
10 | clap = { version = "4.5", features = ["derive"] }
11 | tracing-subscriber = "0.3"
12 | tracing = "0.1"
13 | serde_yaml_ng = "0.10"
14 | serde = { version = "1.0", features = ["derive"] }
15 | serde_json = "1.0"
16 | num_cpus = "1.0"
17 | dashmap = "6.1.0"
18 | once_cell = "1.20"
19 | pingora = { version="0.6", features = ["lb", "openssl"] }
20 | async-trait = "0.1"
21 | lru = "0.16"
22 | openssl = { version = "0.10", features = ["vendored"] }
23 | tokio = { version = "1", features = ["rt"] }
24 | tokio-stream = "0.1"
25 | fnv = "1.0"
26 | matchit = "0.8"
27 | bytes = "1.7"
28 | http = "1.3"
29 | regex = "1.11"
30 | chrono = "0.4"
31 | uuid = { version = "1.13", features = ["v4", "v7"] }
32 | flatbuffers = { version = "25.9", features = ["serde"] }
33 | libloading = "0.8.9"
34 | instant-acme = "0.8"
35 | sha1 = "0.10"
36 | base64 = { version = "0.22", default-features = false, features = ["alloc"] }
37 | redis = { version = "0.32", features = ["aio", "tokio-comp"] }
38 | mime_guess = "2.0"
39 | rcgen = "0.14"
40 | fastrand = "2.1"
41 | service-manager = "0.8"
42 |
43 | [profile.release]
44 | overflow-checks = true
45 | strip = true
46 | opt-level = "z"
47 | lto = true
48 | codegen-units = 1
49 | panic = "abort"
50 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Nylon Documentation
2 |
3 | This directory contains the documentation for Nylon Reverse Proxy, built with [VitePress](https://vitepress.dev/).
4 |
5 | ## Development
6 |
7 | ```bash
8 | # Install dependencies
9 | bun install
10 |
11 | # Start dev server
12 | bun run dev
13 |
14 | # Build for production
15 | bun run build
16 |
17 | # Preview production build
18 | bun run preview
19 | ```
20 |
21 | ## Structure
22 |
23 | ```
24 | docs/
25 | ├── .vitepress/ # VitePress configuration
26 | │ ├── config.ts # Site configuration
27 | │ └── theme/ # Custom theme
28 | ├── introduction/ # Getting started guides
29 | ├── core/ # Core concepts
30 | ├── plugins/ # Plugin development
31 | ├── examples/ # Usage examples
32 | └── api/ # API reference
33 | ```
34 |
35 | ## Writing Documentation
36 |
37 | - Use Markdown (.md) files
38 | - Follow the existing structure
39 | - Add new pages to `.vitepress/config.ts` sidebar
40 | - Use code blocks with language hints
41 | - Add examples where appropriate
42 |
43 | ## Deployment
44 |
45 | The documentation can be deployed to any static hosting service:
46 |
47 | - GitHub Pages
48 | - Netlify
49 | - Vercel
50 | - Cloudflare Pages
51 |
52 | Build the site with `npm run build` and deploy the `.vitepress/dist` directory.
53 |
54 |
--------------------------------------------------------------------------------
/sdk/go/sdk/types.go:
--------------------------------------------------------------------------------
1 | package sdk
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type HttpPluginFunc func(ctx *NylonHttpPluginCtx)
8 |
9 | type NylonPlugin struct{}
10 |
11 | type NylonHttpPluginCtx struct {
12 | sessionID int32
13 |
14 | mu sync.Mutex
15 | cond *sync.Cond
16 | dataMap map[uint32][]byte
17 |
18 | // WebSocket state
19 | wsCallbacks *WebSocketCallbacks
20 | wsUpgraded bool
21 | }
22 |
23 | type Headers struct {
24 | headers map[string]string
25 | }
26 |
27 | type Response struct {
28 | ctx *NylonHttpPluginCtx
29 | }
30 |
31 | type Request struct {
32 | ctx *NylonHttpPluginCtx
33 | }
34 |
35 | type ResponseStream struct {
36 | response *Response
37 | }
38 |
39 | type PhaseRequestFilter struct {
40 | ctx *NylonHttpPluginCtx
41 | }
42 |
43 | type PhaseResponseFilter struct {
44 | ctx *NylonHttpPluginCtx
45 | }
46 |
47 | type PhaseResponseBodyFilter struct {
48 | ctx *NylonHttpPluginCtx
49 | }
50 |
51 | type PhaseLogging struct {
52 | ctx *NylonHttpPluginCtx
53 | }
54 |
55 | // WebSocket types
56 |
57 | type WebSocketConn struct {
58 | ctx *NylonHttpPluginCtx
59 | }
60 |
61 | type WebSocketCallbacks struct {
62 | OnOpen func(ws *WebSocketConn)
63 | OnMessageText func(ws *WebSocketConn, msg string)
64 | OnMessageBinary func(ws *WebSocketConn, data []byte)
65 | OnClose func(ws *WebSocketConn)
66 | OnError func(ws *WebSocketConn, err string)
67 | }
68 |
--------------------------------------------------------------------------------
/crates/nylon-types/src/tls.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Debug, Deserialize, Clone, PartialEq)]
4 | pub enum TlsKind {
5 | #[serde(rename = "custom")]
6 | Custom,
7 | #[serde(rename = "acme")]
8 | Acme,
9 | }
10 |
11 | #[derive(Debug, Deserialize, Clone)]
12 | pub struct TlsConfig {
13 | #[serde(rename = "type")]
14 | pub kind: TlsKind, // "custom" or "acme"
15 | pub key: Option,
16 | pub cert: Option,
17 | pub chain: Option>,
18 | pub acme: Option,
19 | /// Flatten ACME fields to support both nested and flat config formats
20 | #[serde(flatten)]
21 | pub acme_flat: Option,
22 | pub domains: Vec,
23 | }
24 |
25 | #[derive(Debug, Deserialize, Clone)]
26 | pub struct AcmeConfig {
27 | pub provider: String,
28 | pub email: String,
29 | /// Path to ACME directory (will use runtime config if not specified)
30 | #[serde(skip)]
31 | pub acme_dir: Option,
32 | /// Use staging endpoint if provider supports it (e.g. Let's Encrypt)
33 | pub staging: Option,
34 | /// Custom ACME directory URL (overrides provider/staging)
35 | pub directory_url: Option,
36 | /// External Account Binding key identifier (for providers like ZeroSSL)
37 | pub eab_kid: Option,
38 | /// External Account Binding HMAC key (base64/urlsafe as required by provider)
39 | pub eab_hmac_key: Option,
40 | }
41 |
--------------------------------------------------------------------------------
/crates/nylon-types/src/route.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 | use serde_json::Value;
3 |
4 | pub const HTTP_METHODS: [&str; 9] = [
5 | "GET", "POST", "PUT", "DELETE", "OPTIONS", "HEAD", "CONNECT", "TRACE", "PATCH",
6 | ];
7 |
8 | #[derive(Debug, Deserialize, Clone)]
9 | pub struct MiddlewareItem {
10 | pub group: Option,
11 | pub plugin: Option,
12 | pub entry: Option,
13 | pub payload: Option,
14 | }
15 |
16 | #[derive(Debug, Deserialize, Clone)]
17 | pub struct RouteConfig {
18 | pub route: RouteMatcher,
19 | pub name: String,
20 | pub tls: Option,
21 | pub middleware: Option>,
22 | pub paths: Vec,
23 | }
24 |
25 | #[derive(Debug, Deserialize, Clone)]
26 | pub struct RouteMatcher {
27 | #[serde(rename = "type")]
28 | pub kind: String,
29 | pub value: String,
30 | }
31 |
32 | #[derive(Debug, Deserialize, Clone)]
33 | pub struct TlsRoute {
34 | pub enabled: bool,
35 | pub redirect: Option,
36 | }
37 |
38 | #[derive(Debug, Deserialize, Clone)]
39 | pub struct Header {
40 | pub name: String,
41 | pub value: String,
42 | }
43 |
44 | #[derive(Debug, Deserialize, Clone)]
45 | pub struct PathConfig {
46 | pub path: Value,
47 | pub service: ServiceRef,
48 | pub middleware: Option>,
49 | pub methods: Option>,
50 | }
51 |
52 | #[derive(Debug, Deserialize, Clone)]
53 | pub struct ServiceRef {
54 | pub name: String,
55 | pub rewrite: Option,
56 | }
57 |
--------------------------------------------------------------------------------
/crates/nylon-store/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod lb_backends;
2 | pub mod redis_adapter;
3 | pub mod routes;
4 | pub mod tls;
5 | pub mod websocket_adapter;
6 | pub mod websockets;
7 |
8 | use dashmap::DashMap;
9 | use once_cell::sync::Lazy;
10 | use std::any::Any;
11 |
12 | // default values
13 | pub const DEFAULT_HEADER_SELECTOR: &str = "x-nylon-proxy";
14 |
15 | // constants
16 | pub const KEY_RUNTIME_CONFIG: &str = "runtime_config";
17 | pub const KEY_CONFIG_PATH: &str = "config_path";
18 | pub const KEY_COMMAND_SOCKET_PATH: &str = "/tmp/_nylon.sock";
19 | pub const KEY_LB_BACKENDS: &str = "lb_backends";
20 | pub const KEY_ROUTES: &str = "routes";
21 | pub const KEY_TLS_ROUTES: &str = "tls_routes";
22 | pub const KEY_ROUTES_MATCHIT: &str = "routes_matchit";
23 | pub const KEY_HEADER_SELECTOR: &str = "header_selector";
24 | pub const KEY_LIBRARY_FILE: &str = "library_file";
25 | pub const KEY_PLUGINS: &str = "plugins";
26 | pub const KEY_TLS: &str = "tls";
27 | pub const KEY_ACME_CERTS: &str = "acme_certs";
28 | pub const KEY_ACME_CONFIG: &str = "acme_config";
29 | pub const KEY_ACME_METRICS: &str = "acme_metrics";
30 |
31 | // storage for global variables
32 | static GLOBAL_STORE: Lazy>> = Lazy::new(DashMap::new);
33 |
34 | pub fn insert(key: &str, value: T) {
35 | GLOBAL_STORE.insert(key.to_string(), Box::new(value));
36 | }
37 |
38 | pub fn get(key: &str) -> Option {
39 | let entry = GLOBAL_STORE.get(key)?;
40 | let any_ref = entry.downcast_ref::()?;
41 | Some(any_ref.clone())
42 | }
43 |
--------------------------------------------------------------------------------
/examples/proxy/tls.yaml:
--------------------------------------------------------------------------------
1 | tls:
2 | # Custom TLS - Use your own certificate files
3 | - type: custom
4 | cert: ./examples/cert/localhost.crt
5 | key: ./examples/cert/localhost.key
6 | # chain: # Optional - Certificate chain for intermediate CAs
7 | # - ./examples/cert/chain.pem
8 | domains:
9 | - localhost
10 | - app.localhost
11 | - api.localhost
12 | - admin.localhost
13 |
14 | # ACME (Let's Encrypt) - Automatic certificate management
15 | # Requires:
16 | # - Port 80 accessible from internet for HTTP-01 challenge
17 | # - Valid DNS records pointing to your server
18 | # - Email for certificate expiration notifications
19 | #
20 | # Features:
21 | # - Automatic certificate issuance and renewal (30 days before expiry)
22 | # - Rate limiting protection with exponential backoff
23 | # - Challenge token auto-cleanup
24 | # - Comprehensive metrics tracking
25 | # - Retry logic with jitter for failed renewals
26 | #
27 | # - type: acme
28 | # provider: letsencrypt # Options: letsencrypt
29 | # email: admin@example.com # Required for account registration
30 | # # staging: true # Use Let's Encrypt staging for testing (optional)
31 | # # directory_url: https://acme.example.com/directory # Custom ACME server (optional)
32 | # domains:
33 | # - example.com
34 | # - api.example.com
35 | # - admin.example.com
36 | #
37 | # Advanced options (for providers like ZeroSSL, BuyPass):
38 | # Note: Full EAB support is planned but not yet fully implemented
39 | # # eab_kid: your-eab-key-id # External Account Binding key ID
40 | # # eab_hmac_key: your-eab-hmac-key # EAB HMAC key
--------------------------------------------------------------------------------
/examples/cert/localhost.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDZBkRyLRWn+zKa
3 | NjP3AYi8PPUJemyvXblz+VUKjiMSlp+lBqNcxMMY6dZCvhH9EfOEVqtmxvqQv+i3
4 | SW6pKPUv+2ntOF7y7mtlk1O7437F5YpFWwgS2mWSrMVITzd8td7Mq0EaRTesb8Yc
5 | Lf4olXPiFjkiq9w/vItX9LtJf3u3A2Sy2sy554bXTHcUfG0DnB4XV6qyUPoi7q3h
6 | UyB8y8rgnHUmmHLHfsWU/kRHtzLu6HWUPds0DCi2biNUpdgIAx4KOk4cepWg45I7
7 | B0ZkUz58THpNqSJcIEJS1FnFcIbg3lMkN2hiP4vc94jlgEp+/Sem7aP+JyeASqc3
8 | oroy6TVhAgMBAAECggEAGPIfmrysLzaAeOmnVDqCyjlbBKuD8+3q5+XmZKvVI/k4
9 | kUZnr32F+/Z//IS+ylc+ha0VL1+KlGocwLmx/MOCmHD2mnAG+PdXFLJINxIFo894
10 | UvPNZCZis8cEd8T0SLNakG3UckW3yixAakOOogFY1Elv6Jh23QQqA7KTtxVuZekp
11 | miAS6Gdotoz7aiCVNAnw6YKABvheDfRglniCInqZto9MtuPGk/LFcPDVvHVUQVMG
12 | KR5klwj4NZ6HglIk3UT4N4kk8NXBa3ndzZDJq7Lfm3Ns7bHPghrYzD/ABut6R3To
13 | 0MEkCwTlZj5K8d/IT8K77x9B7fqI1GSXusSAlGziWQKBgQDzdP+6KPf3KLORqIqF
14 | k1AakW5+ofwHYgcfes5ZJEpBQAvonJ+dkJkC7HdKoWtpPpbXrMilXdu5jk+hvwC5
15 | Sle9q/M7Pnv9jXLjz+1WjqOScTKy4uvWM+8PX1mOq6/o31gd/3nVwdszLdy3g9DC
16 | VK6X8619bN7PD2QCZqM12QYKmQKBgQDkNKUCIKT2zSD5iTi6BSoCzXhC9O6i+bW2
17 | 0fhsoL38vW/tseGj8aHNSbEfA0AwJJvEzTVOZFsmvVpKhRGXsLBx1MtuRAszoFif
18 | 294LzaooB1gtIhnDY9vjLSoiHPfJV1ASUEmLOtBbPrhLSVq8xithznTGtJwBiAZi
19 | Xe51VlVGCQKBgQC6NkAvVIytOC14+K/TEWUQnTIlm6JYx0rpchYIqrA9Dk7NgZa4
20 | ftP6H4HyzFqKqjvYBSmHCq44VDhmX+Ce2NUZlz64jsdpnVpGE1DWhs1oAjskBlsa
21 | gKiWWnj2ni0zcjlE4JaAwAD4OVj76M+xA/Jy+Qg2yiH1wDDfgT/OvQtY6QKBgQCY
22 | yJo063AmgD10c6eT+0MeLzw179AZMv+yz67340JvhND8HZzI60x9qbm43q9JzCix
23 | wQXQXyYbsKhTvfWCTlxDScmNIGczgEX1ePmXg3FJbWlehjcjdqbP2PwdbLGEjj1g
24 | lXo3if/XJw2x8gGa4z5GNDhAlMjhyZUkpGizDEL5KQKBgQDM6q1/ZJIiPQ9UeUH9
25 | /WgyNI1MgnDdwTC7GjcLSX45DOzv5FCIB0DLC7qcjFY2Xp4hlE9YZs9asE3x4TG1
26 | EbvTTAu/BnLxCDPKQp6FvjqyfrSn/wIcUvB+ZyzGw2PMwzG8DCrIYWopvNSKsHEJ
27 | RKnKARSYSjyCr+Xh7UexNHmdLA==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/sdk/go/fbs/nylon_plugin/RemoveResponseHeader.go:
--------------------------------------------------------------------------------
1 | // Code generated by the FlatBuffers compiler. DO NOT EDIT.
2 |
3 | package nylon_plugin
4 |
5 | import (
6 | flatbuffers "github.com/google/flatbuffers/go"
7 | )
8 |
9 | type RemoveResponseHeader struct {
10 | _tab flatbuffers.Table
11 | }
12 |
13 | func GetRootAsRemoveResponseHeader(buf []byte, offset flatbuffers.UOffsetT) *RemoveResponseHeader {
14 | n := flatbuffers.GetUOffsetT(buf[offset:])
15 | x := &RemoveResponseHeader{}
16 | x.Init(buf, n+offset)
17 | return x
18 | }
19 |
20 | func FinishRemoveResponseHeaderBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
21 | builder.Finish(offset)
22 | }
23 |
24 | func GetSizePrefixedRootAsRemoveResponseHeader(buf []byte, offset flatbuffers.UOffsetT) *RemoveResponseHeader {
25 | n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
26 | x := &RemoveResponseHeader{}
27 | x.Init(buf, n+offset+flatbuffers.SizeUint32)
28 | return x
29 | }
30 |
31 | func FinishSizePrefixedRemoveResponseHeaderBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
32 | builder.FinishSizePrefixed(offset)
33 | }
34 |
35 | func (rcv *RemoveResponseHeader) Init(buf []byte, i flatbuffers.UOffsetT) {
36 | rcv._tab.Bytes = buf
37 | rcv._tab.Pos = i
38 | }
39 |
40 | func (rcv *RemoveResponseHeader) Table() flatbuffers.Table {
41 | return rcv._tab
42 | }
43 |
44 | func (rcv *RemoveResponseHeader) Key() []byte {
45 | o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
46 | if o != 0 {
47 | return rcv._tab.ByteVector(o + rcv._tab.Pos)
48 | }
49 | return nil
50 | }
51 |
52 | func RemoveResponseHeaderStart(builder *flatbuffers.Builder) {
53 | builder.StartObject(1)
54 | }
55 | func RemoveResponseHeaderAddKey(builder *flatbuffers.Builder, key flatbuffers.UOffsetT) {
56 | builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(key), 0)
57 | }
58 | func RemoveResponseHeaderEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
59 | return builder.EndObject()
60 | }
61 |
--------------------------------------------------------------------------------
/crates/nylon-types/src/services.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Debug, Deserialize, Clone)]
4 | pub struct HealthCheck {
5 | pub enabled: bool,
6 | pub path: String,
7 | pub interval: String,
8 | pub timeout: String,
9 | pub healthy_threshold: u32,
10 | pub unhealthy_threshold: u32,
11 | }
12 |
13 | #[derive(Debug, Deserialize, Clone)]
14 | pub struct Endpoint {
15 | pub ip: String,
16 | pub port: u16,
17 | pub weight: Option,
18 | }
19 |
20 | #[derive(Debug, Deserialize, Clone, PartialEq)]
21 | pub enum ServiceType {
22 | #[serde(rename = "http")]
23 | Http,
24 | #[serde(rename = "plugin")]
25 | Plugin,
26 | #[serde(rename = "static")]
27 | Static,
28 | }
29 |
30 | #[derive(Debug, Deserialize, Clone)]
31 | pub enum Algorithm {
32 | #[serde(rename = "round_robin")]
33 | RoundRobin,
34 | #[serde(rename = "random")]
35 | Random,
36 | #[serde(rename = "consistent")]
37 | Consistent,
38 | #[serde(rename = "weighted")]
39 | Weighted,
40 | }
41 |
42 | #[derive(Debug, Deserialize, Clone)]
43 | pub struct Plugin {
44 | pub name: String,
45 | pub entry: String,
46 | pub payload: Option,
47 | }
48 |
49 | #[derive(Debug, Deserialize, Clone)]
50 | pub struct StaticConfig {
51 | /// Root directory to serve files from
52 | pub root: String,
53 | /// Default index file to serve for directories (default: index.html)
54 | pub index: Option,
55 | /// Enable SPA fallback: on 404, serve index file instead
56 | pub spa: Option,
57 | }
58 |
59 | #[derive(Debug, Deserialize, Clone)]
60 | pub struct ServiceItem {
61 | pub name: String,
62 | pub service_type: ServiceType,
63 | pub algorithm: Option,
64 | pub endpoints: Option>,
65 | pub health_check: Option,
66 | pub plugin: Option,
67 | #[serde(rename = "static")]
68 | pub static_conf: Option,
69 | }
70 |
--------------------------------------------------------------------------------
/crates/nylon-config/src/services.rs:
--------------------------------------------------------------------------------
1 | use nylon_error::NylonError;
2 | use nylon_types::services::{Endpoint, HealthCheck};
3 | use std::net::IpAddr;
4 |
5 | pub trait EndpointExt {
6 | fn is_valid_ip(&self) -> Result<(), NylonError>;
7 | }
8 |
9 | pub trait HealthCheckExt {
10 | fn is_valid(&self) -> Result<(), NylonError>;
11 | }
12 |
13 | impl EndpointExt for Endpoint {
14 | fn is_valid_ip(&self) -> Result<(), NylonError> {
15 | match self.ip.parse::() {
16 | Ok(_) => Ok(()),
17 | Err(err) => Err(NylonError::ConfigError(format!(
18 | "Invalid IP address: {}",
19 | err
20 | ))),
21 | }
22 | }
23 | }
24 |
25 | impl HealthCheckExt for HealthCheck {
26 | fn is_valid(&self) -> Result<(), NylonError> {
27 | if self.interval.is_empty() {
28 | return Err(NylonError::ConfigError("Interval must be set".to_string()));
29 | }
30 | if self.timeout.is_empty() {
31 | return Err(NylonError::ConfigError("Timeout must be set".to_string()));
32 | }
33 | if self.healthy_threshold == 0 {
34 | return Err(NylonError::ConfigError(
35 | "Healthy threshold must be set".to_string(),
36 | ));
37 | }
38 | if self.unhealthy_threshold == 0 {
39 | return Err(NylonError::ConfigError(
40 | "Unhealthy threshold must be set".to_string(),
41 | ));
42 | }
43 | if self.path.is_empty() {
44 | return Err(NylonError::ConfigError("Path must be set".to_string()));
45 | }
46 | if !self.interval.ends_with("s") {
47 | return Err(NylonError::ConfigError(
48 | "Interval must be in the format of [0-9]+[s]".to_string(),
49 | ));
50 | }
51 | if !self.timeout.ends_with("s") {
52 | return Err(NylonError::ConfigError(
53 | "Timeout must be in the format of [0-9]+[s]".to_string(),
54 | ));
55 | }
56 | Ok(())
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🧬 Nylon — The Extensible Proxy Server
2 |
3 | [](LICENSE)
4 | [](https://nylon.sh/)
5 |
6 | Nylon is a lightweight, high‑performance, and extensible proxy built on top of the battle‑tested [Cloudflare Pingora](https://github.com/cloudflare/pingora) framework.
7 |
8 | ## ✨ Features
9 |
10 | - **🔌 Extensible**: Write plugins in Go, Rust, Zig, C via FFI
11 | - **📝 Simple YAML Config**: One place to manage routes, services, middleware
12 | - **🎯 Smart Routing**: Host/header/path matching with multiple load balancing algorithms
13 | - **🔒 TLS Built-in**: Custom certificates or ACME (Let's Encrypt, Buypass)
14 | - **☁️ Cloud-native**: Observability and scalability friendly
15 | - **⚡ High Performance**: Built on Cloudflare Pingora framework
16 |
17 | ## 🚀 Quick Start
18 |
19 | ```sh
20 | # Install (macOS/Linux)
21 | curl -fsSL https://nylon.sh/install | sh
22 |
23 | # Run with example config
24 | nylon run -c ./examples/config.yaml
25 | ```
26 |
27 | Test it:
28 | ```sh
29 | curl http://127.0.0.1:8088/
30 | ```
31 |
32 | ## 📖 Documentation
33 |
34 | For complete documentation, visit **[nylon.sh](https://nylon.sh/)**
35 |
36 | - [Installation Guide](https://nylon.sh/introduction/installation)
37 | - [Quick Start](https://nylon.sh/introduction/quick-start)
38 | - [Configuration](https://nylon.sh/core/configuration)
39 | - [Routing & Load Balancing](https://nylon.sh/core/routing)
40 | - [TLS Setup](https://nylon.sh/core/tls)
41 | - [Plugin System](https://nylon.sh/plugins/overview)
42 | - [Examples](https://nylon.sh/examples/basic-proxy)
43 |
44 | ## 🛠️ Build from Source
45 |
46 | ```sh
47 | git clone https://github.com/AssetsArt/nylon.git
48 | cd nylon
49 | make build
50 | ./target/release/nylon run -c ./examples/config.yaml
51 | ```
52 |
53 | ## 🔗 Links
54 |
55 | - 📚 Documentation: [nylon.sh](https://nylon.sh/)
56 | - 🐛 Issues: [GitHub Issues](https://github.com/AssetsArt/nylon/issues)
57 | - 💬 Discussions: [GitHub Discussions](https://github.com/AssetsArt/nylon/discussions)
58 |
59 | ## 📄 License
60 |
61 | MIT Licensed. © AssetsArt.
62 |
--------------------------------------------------------------------------------
/crates/nylon-plugin/src/plugin_manager.rs:
--------------------------------------------------------------------------------
1 | use crate::{constants::builtin_plugins, types::BuiltinPlugin};
2 | use dashmap::DashMap;
3 | use nylon_error::NylonError;
4 | use nylon_types::plugins::FfiPlugin;
5 | use std::sync::Arc;
6 |
7 | pub struct PluginManager;
8 | impl PluginManager {
9 | pub fn try_builtin(name: &str) -> Option {
10 | // tracing::debug!("Trying builtin plugin: {}", name);
11 | match name {
12 | builtin_plugins::REQUEST_HEADER_MODIFIER => Some(BuiltinPlugin::RequestHeaderModifier),
13 | builtin_plugins::RESPONSE_HEADER_MODIFIER => {
14 | Some(BuiltinPlugin::ResponseHeaderModifier)
15 | }
16 | _ => None,
17 | }
18 | }
19 |
20 | pub fn is_request_filter(name: &str) -> bool {
21 | matches!(name, builtin_plugins::REQUEST_HEADER_MODIFIER)
22 | }
23 |
24 | pub fn is_response_filter(name: &str) -> bool {
25 | matches!(name, builtin_plugins::RESPONSE_HEADER_MODIFIER)
26 | }
27 |
28 | pub fn get_plugin(name: &str) -> Result, NylonError> {
29 | let Some(plugins) =
30 | &nylon_store::get::>>(nylon_store::KEY_PLUGINS)
31 | else {
32 | return Err(NylonError::ConfigError("Plugins not found".to_string()));
33 | };
34 |
35 | let Some(plugin) = plugins.get(name) else {
36 | return Err(NylonError::ConfigError(format!(
37 | "Plugin '{}' not found",
38 | name
39 | )));
40 | };
41 |
42 | Ok(plugin.clone())
43 | }
44 |
45 | // /// Get or create a session stream for a plugin
46 | // pub fn get_or_create_session_stream(
47 | // plugin_name: &str,
48 | // ctx: &mut NylonContext,
49 | // ) -> Result {
50 | // let plugin = Self::get_plugin(plugin_name)?;
51 |
52 | // Ok(ctx
53 | // .session_stream
54 | // .entry(plugin_name.to_string())
55 | // .or_insert_with(|| SessionStream::new(plugin))
56 | // .clone())
57 | // }
58 | }
59 |
--------------------------------------------------------------------------------
/sdk/go/fbs/nylon_plugin/HeaderKeyValue.go:
--------------------------------------------------------------------------------
1 | // Code generated by the FlatBuffers compiler. DO NOT EDIT.
2 |
3 | package nylon_plugin
4 |
5 | import (
6 | flatbuffers "github.com/google/flatbuffers/go"
7 | )
8 |
9 | type HeaderKeyValue struct {
10 | _tab flatbuffers.Table
11 | }
12 |
13 | func GetRootAsHeaderKeyValue(buf []byte, offset flatbuffers.UOffsetT) *HeaderKeyValue {
14 | n := flatbuffers.GetUOffsetT(buf[offset:])
15 | x := &HeaderKeyValue{}
16 | x.Init(buf, n+offset)
17 | return x
18 | }
19 |
20 | func FinishHeaderKeyValueBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
21 | builder.Finish(offset)
22 | }
23 |
24 | func GetSizePrefixedRootAsHeaderKeyValue(buf []byte, offset flatbuffers.UOffsetT) *HeaderKeyValue {
25 | n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
26 | x := &HeaderKeyValue{}
27 | x.Init(buf, n+offset+flatbuffers.SizeUint32)
28 | return x
29 | }
30 |
31 | func FinishSizePrefixedHeaderKeyValueBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
32 | builder.FinishSizePrefixed(offset)
33 | }
34 |
35 | func (rcv *HeaderKeyValue) Init(buf []byte, i flatbuffers.UOffsetT) {
36 | rcv._tab.Bytes = buf
37 | rcv._tab.Pos = i
38 | }
39 |
40 | func (rcv *HeaderKeyValue) Table() flatbuffers.Table {
41 | return rcv._tab
42 | }
43 |
44 | func (rcv *HeaderKeyValue) Key() []byte {
45 | o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
46 | if o != 0 {
47 | return rcv._tab.ByteVector(o + rcv._tab.Pos)
48 | }
49 | return nil
50 | }
51 |
52 | func (rcv *HeaderKeyValue) Value() []byte {
53 | o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
54 | if o != 0 {
55 | return rcv._tab.ByteVector(o + rcv._tab.Pos)
56 | }
57 | return nil
58 | }
59 |
60 | func HeaderKeyValueStart(builder *flatbuffers.Builder) {
61 | builder.StartObject(2)
62 | }
63 | func HeaderKeyValueAddKey(builder *flatbuffers.Builder, key flatbuffers.UOffsetT) {
64 | builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(key), 0)
65 | }
66 | func HeaderKeyValueAddValue(builder *flatbuffers.Builder, value flatbuffers.UOffsetT) {
67 | builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(value), 0)
68 | }
69 | func HeaderKeyValueEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
70 | return builder.EndObject()
71 | }
72 |
--------------------------------------------------------------------------------
/sdk/go/fbs/nylon_plugin/NylonHttpHeaders.go:
--------------------------------------------------------------------------------
1 | // Code generated by the FlatBuffers compiler. DO NOT EDIT.
2 |
3 | package nylon_plugin
4 |
5 | import (
6 | flatbuffers "github.com/google/flatbuffers/go"
7 | )
8 |
9 | type NylonHttpHeaders struct {
10 | _tab flatbuffers.Table
11 | }
12 |
13 | func GetRootAsNylonHttpHeaders(buf []byte, offset flatbuffers.UOffsetT) *NylonHttpHeaders {
14 | n := flatbuffers.GetUOffsetT(buf[offset:])
15 | x := &NylonHttpHeaders{}
16 | x.Init(buf, n+offset)
17 | return x
18 | }
19 |
20 | func FinishNylonHttpHeadersBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
21 | builder.Finish(offset)
22 | }
23 |
24 | func GetSizePrefixedRootAsNylonHttpHeaders(buf []byte, offset flatbuffers.UOffsetT) *NylonHttpHeaders {
25 | n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
26 | x := &NylonHttpHeaders{}
27 | x.Init(buf, n+offset+flatbuffers.SizeUint32)
28 | return x
29 | }
30 |
31 | func FinishSizePrefixedNylonHttpHeadersBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
32 | builder.FinishSizePrefixed(offset)
33 | }
34 |
35 | func (rcv *NylonHttpHeaders) Init(buf []byte, i flatbuffers.UOffsetT) {
36 | rcv._tab.Bytes = buf
37 | rcv._tab.Pos = i
38 | }
39 |
40 | func (rcv *NylonHttpHeaders) Table() flatbuffers.Table {
41 | return rcv._tab
42 | }
43 |
44 | func (rcv *NylonHttpHeaders) Headers(obj *HeaderKeyValue, j int) bool {
45 | o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
46 | if o != 0 {
47 | x := rcv._tab.Vector(o)
48 | x += flatbuffers.UOffsetT(j) * 4
49 | x = rcv._tab.Indirect(x)
50 | obj.Init(rcv._tab.Bytes, x)
51 | return true
52 | }
53 | return false
54 | }
55 |
56 | func (rcv *NylonHttpHeaders) HeadersLength() int {
57 | o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
58 | if o != 0 {
59 | return rcv._tab.VectorLen(o)
60 | }
61 | return 0
62 | }
63 |
64 | func NylonHttpHeadersStart(builder *flatbuffers.Builder) {
65 | builder.StartObject(1)
66 | }
67 | func NylonHttpHeadersAddHeaders(builder *flatbuffers.Builder, headers flatbuffers.UOffsetT) {
68 | builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(headers), 0)
69 | }
70 | func NylonHttpHeadersStartHeadersVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
71 | return builder.StartVector(4, numElems, 4)
72 | }
73 | func NylonHttpHeadersEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
74 | return builder.EndObject()
75 | }
76 |
--------------------------------------------------------------------------------
/crates/nylon-types/src/plugins.rs:
--------------------------------------------------------------------------------
1 | use libloading::{Library, Symbol};
2 | use serde::{Deserialize, Serialize};
3 | use std::sync::Arc;
4 |
5 | #[derive(Debug, Clone)]
6 | pub enum PluginPhase {
7 | Zero,
8 | RequestFilter,
9 | ResponseFilter,
10 | ResponseBodyFilter,
11 | Logging,
12 | }
13 |
14 | impl PluginPhase {
15 | pub fn to_u8(self) -> u8 {
16 | match self {
17 | PluginPhase::Zero => 0,
18 | PluginPhase::RequestFilter => 1,
19 | PluginPhase::ResponseFilter => 2,
20 | PluginPhase::ResponseBodyFilter => 3,
21 | PluginPhase::Logging => 4,
22 | }
23 | }
24 | }
25 |
26 | #[repr(C)]
27 | pub struct FfiBuffer {
28 | pub sid: u32,
29 | pub phase: u8,
30 | pub method: u32,
31 | pub ptr: *const u8,
32 | pub len: u64,
33 | }
34 |
35 | #[derive(Debug, Deserialize, Serialize, Clone)]
36 | pub enum PluginType {
37 | #[serde(rename = "wasm")]
38 | Wasm,
39 | #[serde(rename = "ffi")]
40 | Ffi,
41 | }
42 |
43 | #[derive(Debug, Deserialize, Serialize, Clone)]
44 | pub struct LifeCycle {
45 | pub setup: Option,
46 | pub shutdown: Option,
47 | }
48 |
49 | #[derive(Debug, Deserialize, Serialize, Clone)]
50 | pub struct PluginItem {
51 | pub name: String,
52 | pub file: String,
53 | #[serde(rename = "type")]
54 | pub plugin_type: PluginType,
55 | pub entry: Option>,
56 | pub config: Option,
57 | }
58 |
59 | // FFI Plugin
60 | pub type FfiInitializeFn = unsafe extern "C" fn(*const u8, u32);
61 | pub type FfiPluginFreeFn = unsafe extern "C" fn(*mut u8);
62 | pub type FfiRegisterSessionFn =
63 | unsafe extern "C" fn(u32, *const u8, u32, extern "C" fn(*const FfiBuffer)) -> bool;
64 | pub type FfiEventStreamFn = unsafe extern "C" fn(*const FfiBuffer);
65 | pub type FfiCloseSessionFn = unsafe extern "C" fn(u32);
66 | pub type FfiShutdownFn = unsafe extern "C" fn();
67 |
68 | #[derive(Debug)]
69 | pub struct FfiPlugin {
70 | pub _lib: Arc,
71 | pub plugin_free: Symbol<'static, FfiPluginFreeFn>,
72 | pub register_session: Symbol<'static, FfiRegisterSessionFn>,
73 | pub event_stream: Symbol<'static, FfiEventStreamFn>,
74 | pub close_session: Symbol<'static, FfiCloseSessionFn>,
75 | pub shutdown: Symbol<'static, FfiShutdownFn>,
76 | }
77 |
78 | // Plugin Session Stream
79 | #[derive(Debug, Clone)]
80 | pub struct SessionStream {
81 | pub plugin: Arc,
82 | pub session_id: u32,
83 | }
84 |
--------------------------------------------------------------------------------
/docs/.vitepress/theme/custom.css:
--------------------------------------------------------------------------------
1 | /**
2 | * Customize default theme styling by overriding CSS variables:
3 | * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
4 | */
5 |
6 | /**
7 | * Colors
8 | * -------------------------------------------------------------------------- */
9 |
10 | :root {
11 | --vp-c-brand-1: #646cff;
12 | --vp-c-brand-2: #747bff;
13 | --vp-c-brand-3: #535bf2;
14 | --vp-c-brand-soft: rgba(100, 108, 255, 0.14);
15 | }
16 |
17 | .dark {
18 | --vp-c-brand-1: #a8b1ff;
19 | --vp-c-brand-2: #8890ff;
20 | --vp-c-brand-3: #6873ff;
21 | --vp-c-brand-soft: rgba(100, 108, 255, 0.16);
22 | }
23 |
24 | /**
25 | * Component: Button
26 | * -------------------------------------------------------------------------- */
27 |
28 | :root {
29 | --vp-button-brand-border: transparent;
30 | --vp-button-brand-text: var(--vp-c-white);
31 | --vp-button-brand-bg: var(--vp-c-brand-3);
32 | --vp-button-brand-hover-border: transparent;
33 | --vp-button-brand-hover-text: var(--vp-c-white);
34 | --vp-button-brand-hover-bg: var(--vp-c-brand-2);
35 | --vp-button-brand-active-border: transparent;
36 | --vp-button-brand-active-text: var(--vp-c-white);
37 | --vp-button-brand-active-bg: var(--vp-c-brand-1);
38 | }
39 |
40 | /**
41 | * Component: Home
42 | * -------------------------------------------------------------------------- */
43 |
44 | :root {
45 | --vp-home-hero-name-color: transparent;
46 | --vp-home-hero-name-background: -webkit-linear-gradient(
47 | 120deg,
48 | #bd34fe 30%,
49 | #41d1ff
50 | );
51 |
52 | --vp-home-hero-image-background-image: linear-gradient(
53 | -45deg,
54 | #bd34fe 50%,
55 | #47caff 50%
56 | );
57 | --vp-home-hero-image-filter: blur(44px);
58 | }
59 |
60 | @media (min-width: 640px) {
61 | :root {
62 | --vp-home-hero-image-filter: blur(56px);
63 | }
64 | }
65 |
66 | @media (min-width: 960px) {
67 | :root {
68 | --vp-home-hero-image-filter: blur(68px);
69 | }
70 | }
71 |
72 | /**
73 | * Component: Custom Block
74 | * -------------------------------------------------------------------------- */
75 |
76 | :root {
77 | --vp-custom-block-tip-border: transparent;
78 | --vp-custom-block-tip-text: var(--vp-c-text-1);
79 | --vp-custom-block-tip-bg: var(--vp-c-brand-soft);
80 | --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
81 | }
82 |
83 | /**
84 | * Component: Algolia
85 | * -------------------------------------------------------------------------- */
86 |
87 | .DocSearch {
88 | --docsearch-primary-color: var(--vp-c-brand-1) !important;
89 | }
90 |
91 |
--------------------------------------------------------------------------------
/examples/proxy/base.yaml:
--------------------------------------------------------------------------------
1 | header_selector: x-nylon-proxy
2 |
3 | plugins:
4 | - name: plugin_sdk
5 | type: ffi
6 | file: ./target/examples/go/plugin_sdk.so
7 | config:
8 | debug: true
9 |
10 | services:
11 | # Http service
12 | - name: app-service
13 | service_type: http
14 | algorithm: round_robin # Options: round_robin, random, consistent, weighted
15 | endpoints:
16 | - ip: 127.0.0.1
17 | port: 3001
18 | - ip: 127.0.0.1
19 | port: 3002
20 | health_check:
21 | enabled: false
22 | path: /health
23 | interval: 3s
24 | timeout: 1s
25 | healthy_threshold: 2
26 | unhealthy_threshold: 2
27 |
28 | # WebSocket and streaming via plugin
29 | - name: ws-service
30 | service_type: plugin
31 | plugin:
32 | name: plugin_sdk
33 | entry: ws
34 |
35 | - name: stream-service
36 | service_type: plugin
37 | plugin:
38 | name: plugin_sdk
39 | entry: stream
40 |
41 | # Static assets / SPA
42 | - name: static
43 | service_type: static
44 | static:
45 | root: ./examples/static
46 | index: index.html
47 | spa: true
48 |
49 | - name: myapp-service
50 | service_type: plugin
51 | plugin:
52 | name: plugin_sdk
53 | entry: myapp
54 |
55 | middleware_groups:
56 | # Security & request metadata headers applied to most routes
57 | security:
58 | - plugin: RequestHeaderModifier
59 | payload:
60 | remove:
61 | - x-version
62 | set:
63 | - name: x-request-id
64 | value: "${uuid(v7)}"
65 | - name: x-forwarded-for
66 | value: "${request(client_ip)}"
67 | - name: x-host
68 | value: "${header(host)}"
69 | - name: x-timestamp
70 | value: "${timestamp()}"
71 | - plugin: ResponseHeaderModifier
72 | payload:
73 | set:
74 | - name: cache-control
75 | value: "no-store"
76 | - name: referrer-policy
77 | value: "no-referrer"
78 | - name: x-content-type-options
79 | value: "nosniff"
80 | - name: x-frame-options
81 | value: "DENY"
82 | - name: content-security-policy
83 | value: "default-src 'self'"
84 | - name: x-server
85 | value: ${or(env(SERVER_NAME), 'nylon-demo')}
86 |
87 | # Authorization example using FFI plugin
88 | auth:
89 | - plugin: plugin_sdk
90 | entry: "authz"
91 | payload:
92 | client_ip: "${request(client_ip)}"
93 | user_id: "${header(x-user-id)}"
94 |
--------------------------------------------------------------------------------
/.github/workflows/Ci.yml:
--------------------------------------------------------------------------------
1 | name: Build, Publish Docker Image, and Release Binaries
2 |
3 | on:
4 | push:
5 | branches:
6 | - release
7 |
8 | jobs:
9 | build-and-release-binaries:
10 | permissions: write-all
11 | runs-on: ubuntu-latest
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v3
15 |
16 | - name: Set up Rust
17 | uses: actions-rust-lang/setup-rust-toolchain@v1
18 | with:
19 | toolchain: stable
20 |
21 | - name: Install cross
22 | run: cargo install cross --git https://github.com/cross-rs/cross
23 |
24 | - name: Install jq
25 | run: sudo apt-get install -y jq
26 |
27 | - name: Extract version from Cargo.toml
28 | id: extract_version
29 | run: |
30 | VERSION=$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[] | select(.name == "nylon") | .version')
31 | echo "version=$VERSION" >> $GITHUB_OUTPUT
32 |
33 | - name: Build Linux binary x86_64-gnu
34 | run: |
35 | cross build --target x86_64-unknown-linux-gnu --release
36 | mkdir -p build/linux/
37 | cp target/x86_64-unknown-linux-gnu/release/nylon build/linux/nylon-x86_64-linux-gnu
38 |
39 | - name: Build Linux binary x86_64-musl
40 | run: |
41 | cross build --target x86_64-unknown-linux-musl --release
42 | mkdir -p build/linux/
43 | cp target/x86_64-unknown-linux-musl/release/nylon build/linux/nylon-x86_64-linux-musl
44 |
45 | - name: Build Linux binary aarch64-gnu
46 | run: |
47 | cross build --target aarch64-unknown-linux-gnu --release
48 | mkdir -p build/linux/
49 | cp target/aarch64-unknown-linux-gnu/release/nylon build/linux/nylon-aarch64-linux-gnu
50 |
51 | - name: Build Linux binary aarch64-musl
52 | run: |
53 | cross build --target aarch64-unknown-linux-musl --release
54 | mkdir -p build/linux/
55 | cp target/aarch64-unknown-linux-musl/release/nylon build/linux/nylon-aarch64-linux-musl
56 |
57 | - name: Generate Checksums
58 | run: |
59 | cd build/linux
60 | shasum -a 256 * > linux-checksums.txt
61 |
62 | - name: Push binaries to release
63 | uses: softprops/action-gh-release@v1
64 | with:
65 | repository: ${{ github.repository }}
66 | files: |
67 | build/linux/nylon-x86_64-linux-gnu
68 | build/linux/nylon-x86_64-linux-musl
69 | build/linux/nylon-aarch64-linux-gnu
70 | build/linux/nylon-aarch64-linux-musl
71 | build/linux/linux-checksums.txt
72 | tag_name: v${{ steps.extract_version.outputs.version }}
73 | name: v${{ steps.extract_version.outputs.version }}
74 | token: ${{ secrets.GITHUB_TOKEN }}
75 |
--------------------------------------------------------------------------------
/docs/.vitepress/config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitepress'
2 |
3 | export default defineConfig({
4 | title: 'Nylon',
5 | description: 'High-performance HTTP/HTTPS reverse proxy with plugin support',
6 |
7 | head: [
8 | ['link', { rel: 'icon', type: 'image/svg+xml', href: '/logo.svg' }],
9 | ],
10 |
11 | themeConfig: {
12 | logo: '/logo.svg',
13 |
14 | nav: [
15 | { text: 'Guide', link: '/introduction/what-is-nylon' },
16 | { text: 'Plugins', link: '/plugins/overview' },
17 | { text: 'API', link: '/api/configuration' },
18 | { text: 'Examples', link: '/examples/basic-proxy' },
19 | ],
20 |
21 | sidebar: {
22 | '/': [
23 | {
24 | text: 'Introduction',
25 | items: [
26 | { text: 'What is Nylon?', link: '/introduction/what-is-nylon' },
27 | { text: 'Installation', link: '/introduction/installation' },
28 | { text: 'Quick Start', link: '/introduction/quick-start' },
29 | ]
30 | },
31 | {
32 | text: 'Core Concepts',
33 | items: [
34 | { text: 'Configuration', link: '/core/configuration' },
35 | { text: 'Routing', link: '/core/routing' },
36 | { text: 'Load Balancing', link: '/core/load-balancing' },
37 | { text: 'TLS/HTTPS', link: '/core/tls' },
38 | { text: 'Middleware', link: '/core/middleware' },
39 | ]
40 | },
41 | {
42 | text: 'Plugin Development',
43 | items: [
44 | { text: 'Overview', link: '/plugins/overview' },
45 | { text: 'Plugin Phases', link: '/plugins/phases' },
46 | { text: 'Go SDK', link: '/plugins/go-sdk' },
47 | { text: 'Request Handling', link: '/plugins/request' },
48 | { text: 'Response Handling', link: '/plugins/response' },
49 | { text: 'WebSocket Support', link: '/plugins/websocket' },
50 | ]
51 | },
52 | {
53 | text: 'Examples',
54 | items: [
55 | { text: 'Basic Proxy', link: '/examples/basic-proxy' },
56 | { text: 'Authentication', link: '/examples/authentication' },
57 | { text: 'Rate Limiting', link: '/examples/rate-limiting' },
58 | { text: 'Custom Headers', link: '/examples/custom-headers' },
59 | { text: 'WebSocket Proxy', link: '/examples/websocket' },
60 | ]
61 | },
62 | {
63 | text: 'API Reference',
64 | items: [
65 | { text: 'Configuration Schema', link: '/api/configuration' },
66 | { text: 'Go SDK API', link: '/api/go-sdk' },
67 | ]
68 | }
69 | ]
70 | },
71 |
72 | socialLinks: [
73 | { icon: 'github', link: 'https://github.com/AssetsArt/nylon' }
74 | ],
75 |
76 | footer: {
77 | message: 'Released under the MIT License.',
78 | copyright: 'Copyright © 2025-present'
79 | },
80 |
81 | search: {
82 | provider: 'local'
83 | }
84 | }
85 | })
86 |
87 |
--------------------------------------------------------------------------------
/crates/nylon-plugin/src/constants.rs:
--------------------------------------------------------------------------------
1 | //! Constants used throughout the plugin system
2 |
3 | // Plugin method constants
4 | pub mod methods {
5 | // Control methods
6 | pub const NEXT: u32 = 1;
7 | pub const END: u32 = 2;
8 | pub const GET_PAYLOAD: u32 = 3;
9 |
10 | // Response methods
11 | pub const SET_RESPONSE_HEADER: u32 = 100;
12 | pub const REMOVE_RESPONSE_HEADER: u32 = 101;
13 | pub const SET_RESPONSE_STATUS: u32 = 102;
14 | pub const SET_RESPONSE_FULL_BODY: u32 = 103;
15 | pub const SET_RESPONSE_STREAM_DATA: u32 = 104;
16 | pub const SET_RESPONSE_STREAM_END: u32 = 105;
17 | pub const SET_RESPONSE_STREAM_HEADER: u32 = 106;
18 | pub const READ_RESPONSE_FULL_BODY: u32 = 107;
19 | pub const READ_RESPONSE_STATUS: u32 = 108;
20 | pub const READ_RESPONSE_BYTES: u32 = 109;
21 | pub const READ_RESPONSE_HEADERS: u32 = 110;
22 | pub const READ_RESPONSE_DURATION: u32 = 111;
23 | pub const READ_RESPONSE_ERROR: u32 = 112;
24 |
25 | // Request methods
26 | pub const READ_REQUEST_FULL_BODY: u32 = 200;
27 | pub const READ_REQUEST_HEADER: u32 = 201;
28 | pub const READ_REQUEST_HEADERS: u32 = 202;
29 | pub const READ_REQUEST_URL: u32 = 203;
30 | pub const READ_REQUEST_PATH: u32 = 204;
31 | pub const READ_REQUEST_QUERY: u32 = 205;
32 | pub const READ_REQUEST_PARAMS: u32 = 206;
33 | pub const READ_REQUEST_HOST: u32 = 207;
34 | pub const READ_REQUEST_CLIENT_IP: u32 = 208;
35 | pub const READ_REQUEST_METHOD: u32 = 209;
36 | pub const READ_REQUEST_BYTES: u32 = 210;
37 | pub const READ_REQUEST_TIMESTAMP: u32 = 211;
38 |
39 | // WebSocket methods (Plugin -> Rust)
40 | pub const WEBSOCKET_UPGRADE: u32 = 300;
41 | pub const WEBSOCKET_SEND_TEXT: u32 = 301;
42 | pub const WEBSOCKET_SEND_BINARY: u32 = 302;
43 | pub const WEBSOCKET_CLOSE: u32 = 303;
44 |
45 | // WebSocket room methods (Plugin -> Rust)
46 | pub const WEBSOCKET_JOIN_ROOM: u32 = 310;
47 | pub const WEBSOCKET_LEAVE_ROOM: u32 = 311;
48 | pub const WEBSOCKET_BROADCAST_ROOM_TEXT: u32 = 312;
49 | pub const WEBSOCKET_BROADCAST_ROOM_BINARY: u32 = 313;
50 |
51 | // WebSocket events (Rust -> Plugin)
52 | pub const WEBSOCKET_ON_OPEN: u32 = 350;
53 | pub const WEBSOCKET_ON_MESSAGE_TEXT: u32 = 351;
54 | pub const WEBSOCKET_ON_MESSAGE_BINARY: u32 = 352;
55 | pub const WEBSOCKET_ON_CLOSE: u32 = 353;
56 | pub const WEBSOCKET_ON_ERROR: u32 = 354;
57 | }
58 |
59 | // FFI symbol names
60 | pub mod ffi_symbols {
61 | pub const INITIALIZE: &str = "initialize";
62 | pub const PLUGIN_FREE: &str = "plugin_free";
63 | pub const REGISTER_SESSION: &str = "register_session_stream";
64 | pub const EVENT_STREAM: &str = "event_stream";
65 | pub const CLOSE_SESSION: &str = "close_session_stream";
66 | pub const SHUTDOWN: &str = "shutdown";
67 | }
68 |
69 | // Builtin plugin names
70 | pub mod builtin_plugins {
71 | pub const REQUEST_HEADER_MODIFIER: &str = "RequestHeaderModifier";
72 | pub const RESPONSE_HEADER_MODIFIER: &str = "ResponseHeaderModifier";
73 | }
74 |
--------------------------------------------------------------------------------
/crates/nylon/src/dynamic_certificate.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use nylon_error::NylonError;
3 | use nylon_store::{routes, tls};
4 | use openssl::{
5 | pkey::PKey,
6 | ssl::{NameType, SslRef},
7 | x509::X509,
8 | };
9 | use pingora::{
10 | listeners::{TlsAccept, tls::TlsSettings},
11 | tls::ext,
12 | };
13 | use tracing::error;
14 |
15 | #[derive(Default)]
16 | pub struct DynamicCertificate;
17 |
18 | impl DynamicCertificate {
19 | pub fn new() -> Self {
20 | Self
21 | }
22 | }
23 |
24 | pub fn new_tls_settings() -> Result {
25 | let mut tls = TlsSettings::with_callbacks(Box::new(DynamicCertificate::new()))
26 | .map_err(|e| NylonError::PingoraError(e.to_string()))?;
27 | tls.enable_h2();
28 | Ok(tls)
29 | }
30 |
31 | #[async_trait]
32 | impl TlsAccept for DynamicCertificate {
33 | async fn certificate_callback(&self, ssl: &mut SslRef) {
34 | let server_name = ssl.servername(NameType::HOST_NAME);
35 |
36 | let server_name = match server_name {
37 | Some(s) => s,
38 | None => {
39 | error!("Unable to get server name");
40 | "localhost"
41 | }
42 | };
43 |
44 | // Enabled tls route?
45 | if let Err(e) = routes::get_tls_route(server_name) {
46 | error!("Unable to get TLS route: {}", e);
47 | return;
48 | }
49 | // debug!("server_name: {}", server_name);
50 | let tls_store = match tls::get_certs(server_name) {
51 | Ok(tls_store) => tls_store,
52 | Err(e) => {
53 | error!("Unable to get TLS store: {}", e);
54 | return;
55 | }
56 | };
57 | // debug!("tls_store: {:?}", tls_store);
58 | let cert = match X509::from_pem(&tls_store.cert) {
59 | Ok(cert) => cert,
60 | Err(e) => {
61 | error!("Failed to parse certificate: {}", e);
62 | return;
63 | }
64 | };
65 | let key = match PKey::private_key_from_pem(&tls_store.key) {
66 | Ok(key) => key,
67 | Err(e) => {
68 | error!("Failed to parse private key: {}", e);
69 | return;
70 | }
71 | };
72 |
73 | if let Err(e) = ext::ssl_use_certificate(ssl, &cert) {
74 | error!("Failed to use certificate: {}", e);
75 | }
76 |
77 | if let Err(e) = ext::ssl_use_private_key(ssl, &key) {
78 | error!("Failed to use private key: {}", e);
79 | }
80 |
81 | for chain in &tls_store.chain {
82 | let chain = match X509::from_pem(chain) {
83 | Ok(chain) => chain,
84 | Err(e) => {
85 | error!("Failed to parse chain certificate: {}", e);
86 | return;
87 | }
88 | };
89 | if let Err(e) = ext::ssl_add_chain_cert(ssl, &chain) {
90 | error!("Failed to add chain certificate: {}", e);
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/crates/nylon-plugin/src/native/header_modifier.rs:
--------------------------------------------------------------------------------
1 | use nylon_error::NylonError;
2 | use nylon_types::{
3 | context::NylonContext,
4 | template::{Expr, apply_payload_ast},
5 | };
6 | use pingora::proxy::Session;
7 | use serde::Deserialize;
8 | use serde_json::Value;
9 | use std::collections::HashMap;
10 |
11 | /// Payload structure for header modification
12 | #[derive(Debug, Deserialize, Clone)]
13 | struct Payload {
14 | remove: Option>,
15 | set: Option>,
16 | }
17 |
18 | /// Header structure for setting headers
19 | #[derive(Debug, Deserialize, Clone)]
20 | struct Header {
21 | name: String,
22 | value: String,
23 | }
24 |
25 | pub fn request(
26 | ctx: &mut NylonContext,
27 | session: &mut Session,
28 | payload: &Option,
29 | payload_ast: &Option>>,
30 | ) -> Result<(), NylonError> {
31 | let headers = session.req_header_mut();
32 | let payload = match payload.as_ref() {
33 | Some(payload) => {
34 | let mut payload = payload.clone();
35 | if let Some(payload_ast) = payload_ast {
36 | apply_payload_ast(&mut payload, payload_ast, headers, ctx);
37 | }
38 | serde_json::from_value::(payload.clone())
39 | .map_err(|e| NylonError::ConfigError(e.to_string()))?
40 | }
41 | None => Payload {
42 | remove: None,
43 | set: None,
44 | },
45 | };
46 | // println!("payload: {:#?}", payload);
47 | if let Some(set) = payload.set {
48 | for header in set {
49 | let _ = headers.remove_header(&header.name);
50 | let name = header.name.to_ascii_lowercase();
51 | let _ = headers.append_header(name.clone(), &header.value);
52 | }
53 | }
54 | if let Some(remove) = payload.remove {
55 | for header in remove {
56 | let _ = headers.remove_header(&header.to_ascii_lowercase());
57 | }
58 | }
59 | Ok(())
60 | }
61 |
62 | pub fn response(
63 | ctx: &mut NylonContext,
64 | session: &mut Session,
65 | payload: &Option,
66 | payload_ast: &Option>>,
67 | ) -> Result<(), NylonError> {
68 | let headers = session.req_header();
69 | let payload = match payload.as_ref() {
70 | Some(payload) => {
71 | let mut payload = payload.clone();
72 | if let Some(payload_ast) = payload_ast {
73 | apply_payload_ast(&mut payload, payload_ast, headers, ctx);
74 | }
75 | serde_json::from_value::(payload.clone())
76 | .map_err(|e| NylonError::ConfigError(e.to_string()))?
77 | }
78 | None => Payload {
79 | remove: None,
80 | set: None,
81 | },
82 | };
83 | if let Some(set) = payload.set {
84 | let mut map = ctx.add_response_header.write().expect("lock");
85 | for header in set {
86 | let _ = map.insert(header.name, header.value);
87 | }
88 | }
89 | if let Some(remove) = payload.remove {
90 | let mut vec = ctx.remove_response_header.write().expect("lock");
91 | for header in remove {
92 | vec.push(header);
93 | }
94 | }
95 | Ok(())
96 | }
97 |
--------------------------------------------------------------------------------
/crates/nylon-types/src/websocket.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use std::collections::HashMap;
3 | use tokio::sync::mpsc;
4 |
5 | /// WebSocket adapter configuration
6 | #[derive(Debug, Clone, Serialize, Deserialize)]
7 | pub struct WebSocketAdapterConfig {
8 | pub adapter_type: AdapterType,
9 | pub redis: Option,
10 | pub cluster: Option,
11 | }
12 |
13 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14 | pub enum AdapterType {
15 | #[serde(rename = "memory")]
16 | Memory,
17 | #[serde(rename = "redis")]
18 | Redis,
19 | #[serde(rename = "cluster")]
20 | Cluster,
21 | }
22 |
23 | #[derive(Debug, Clone, Serialize, Deserialize)]
24 | pub struct RedisAdapterConfig {
25 | pub host: String,
26 | pub port: u16,
27 | pub password: Option,
28 | pub db: Option,
29 | pub key_prefix: Option,
30 | }
31 |
32 | #[derive(Debug, Clone, Serialize, Deserialize)]
33 | pub struct ClusterAdapterConfig {
34 | pub nodes: Vec,
35 | pub key_prefix: Option,
36 | }
37 |
38 | /// WebSocket connection information
39 | #[derive(Debug, Clone, Serialize, Deserialize)]
40 | pub struct WebSocketConnection {
41 | pub id: String,
42 | pub session_id: u32,
43 | pub rooms: Vec,
44 | pub node_id: String,
45 | pub connected_at: u64,
46 | pub metadata: HashMap,
47 | }
48 |
49 | /// WebSocket room information
50 | #[derive(Debug, Clone, Serialize, Deserialize)]
51 | pub struct WebSocketRoom {
52 | pub name: String,
53 | pub connections: Vec,
54 | pub created_at: u64,
55 | pub metadata: HashMap,
56 | }
57 |
58 | /// WebSocket event types for cluster communication
59 | #[derive(Debug, Clone, Serialize, Deserialize)]
60 | pub enum WebSocketEvent {
61 | /// Connection joined a room
62 | JoinRoom {
63 | connection_id: String,
64 | room: String,
65 | node_id: String,
66 | },
67 | /// Connection left a room
68 | LeaveRoom {
69 | connection_id: String,
70 | room: String,
71 | node_id: String,
72 | },
73 | /// Connection disconnected
74 | Disconnect {
75 | connection_id: String,
76 | node_id: String,
77 | },
78 | /// Broadcast message to room
79 | BroadcastToRoom {
80 | room: String,
81 | message: WebSocketMessage,
82 | exclude_connection: Option,
83 | sender_node_id: String,
84 | },
85 | /// Send message to specific connection
86 | SendToConnection {
87 | connection_id: String,
88 | message: WebSocketMessage,
89 | sender_node_id: String,
90 | },
91 | }
92 |
93 | /// WebSocket message types
94 | #[derive(Debug, Clone, Serialize, Deserialize)]
95 | pub enum WebSocketMessage {
96 | Text(String),
97 | Binary(Vec),
98 | Close { code: u16, reason: String },
99 | Ping(Vec),
100 | Pong(Vec),
101 | }
102 |
103 | /// Adapter event sender type
104 | pub type AdapterEventSender = mpsc::UnboundedSender;
105 |
106 | /// Adapter event receiver type
107 | pub type AdapterEventReceiver = mpsc::UnboundedReceiver;
108 |
--------------------------------------------------------------------------------
/docs/public/logo.svg:
--------------------------------------------------------------------------------
1 |
66 |
--------------------------------------------------------------------------------
/crates/nylon-tls/src/certificate.rs:
--------------------------------------------------------------------------------
1 | use chrono::{DateTime, NaiveDateTime, Utc};
2 | use nylon_error::NylonError;
3 | use openssl::asn1::Asn1TimeRef;
4 | use openssl::x509::X509;
5 | use serde::{Deserialize, Serialize};
6 |
7 | /// ข้อมูล certificate พร้อมวันหมดอายุ
8 | #[derive(Debug, Clone, Serialize, Deserialize)]
9 | pub struct CertificateInfo {
10 | pub domain: String,
11 | pub cert: Vec,
12 | pub key: Vec,
13 | pub chain: Vec>,
14 | pub expires_at: DateTime,
15 | pub issued_at: DateTime,
16 | }
17 |
18 | impl CertificateInfo {
19 | /// สร้าง CertificateInfo จาก certificate และ key
20 | pub fn new(
21 | domain: String,
22 | cert: Vec,
23 | key: Vec,
24 | chain: Vec>,
25 | ) -> Result {
26 | let x509 = X509::from_pem(&cert)
27 | .map_err(|e| NylonError::ConfigError(format!("Failed to parse certificate: {}", e)))?;
28 |
29 | let not_after = x509.not_after();
30 | let not_before = x509.not_before();
31 |
32 | // แปลง ASN1Time เป็น DateTime
33 | let expires_at = asn1_time_to_datetime(not_after)?;
34 | let issued_at = asn1_time_to_datetime(not_before)?;
35 |
36 | Ok(Self {
37 | domain,
38 | cert,
39 | key,
40 | chain,
41 | expires_at,
42 | issued_at,
43 | })
44 | }
45 |
46 | /// ตรวจสอบว่า certificate ใกล้หมดอายุหรือไม่ (น้อยกว่า 30 วัน)
47 | pub fn needs_renewal(&self) -> bool {
48 | let now = Utc::now();
49 | let days_until_expiry = (self.expires_at - now).num_days();
50 | days_until_expiry < 30
51 | }
52 |
53 | /// ตรวจสอบว่า certificate หมดอายุแล้วหรือไม่
54 | pub fn is_expired(&self) -> bool {
55 | Utc::now() > self.expires_at
56 | }
57 |
58 | /// คำนวณจำนวนวันที่เหลือจนถึงวันหมดอายุ
59 | pub fn days_until_expiry(&self) -> i64 {
60 | (self.expires_at - Utc::now()).num_days()
61 | }
62 | }
63 |
64 | /// แปลง ASN1Time เป็น DateTime โดย parse string format
65 | fn asn1_time_to_datetime(asn1_time: &Asn1TimeRef) -> Result, NylonError> {
66 | // ASN1Time format examples:
67 | // "Oct 5 10:02:11 2025 GMT" (2 spaces for single digit day)
68 | // "Oct 15 10:02:11 2025 GMT" (1 space for double digit day)
69 | let time_str = asn1_time.to_string();
70 |
71 | // Try parsing with various formats
72 | // Format 1: "Oct 5 10:02:11 2025 GMT" (with GMT suffix)
73 | if let Ok(naive) = NaiveDateTime::parse_from_str(&time_str, "%b %e %H:%M:%S %Y GMT") {
74 | return Ok(DateTime::::from_naive_utc_and_offset(naive, Utc));
75 | }
76 |
77 | // Format 2: "Oct 5 10:02:11 2025" (without GMT suffix)
78 | if let Ok(naive) = NaiveDateTime::parse_from_str(&time_str, "%b %e %H:%M:%S %Y") {
79 | return Ok(DateTime::::from_naive_utc_and_offset(naive, Utc));
80 | }
81 |
82 | // Format 3: Try with explicit timezone
83 | if let Ok(dt) = DateTime::parse_from_str(&time_str, "%b %e %H:%M:%S %Y %Z") {
84 | return Ok(dt.to_utc());
85 | }
86 |
87 | Err(NylonError::ConfigError(format!(
88 | "Failed to parse ASN1 time: {}",
89 | time_str
90 | )))
91 | }
92 |
93 | /// Certificate store สำหรับเก็บข้อมูล ACME certificates
94 | #[derive(Debug, Clone, Default)]
95 | pub struct CertificateStore;
96 |
97 | impl CertificateStore {
98 | pub fn new() -> Self {
99 | Self
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/crates/nylon/src/response.rs:
--------------------------------------------------------------------------------
1 | use crate::runtime::NylonRuntime;
2 | use bytes::Bytes;
3 | use nylon_types::context::NylonContext;
4 | use pingora::{
5 | ErrorType,
6 | http::ResponseHeader,
7 | protocols::http::HttpTask,
8 | proxy::{ProxyHttp, Session},
9 | };
10 | use serde_json::Value;
11 |
12 | pub struct Response<'a> {
13 | pub body: Option,
14 | pub proxy: &'a NylonRuntime,
15 | pub ctx: &'a mut NylonContext,
16 | }
17 |
18 | impl<'a> Response<'a> {
19 | pub async fn new(proxy: &'a NylonRuntime, ctx: &'a mut NylonContext) -> pingora::Result {
20 | Ok(Self {
21 | body: None,
22 | proxy,
23 | ctx,
24 | })
25 | }
26 |
27 | pub fn redirect(&mut self, redirect: String) -> &mut Self {
28 | self.status(301);
29 | {
30 | let mut headers = self.ctx.add_response_header.write().expect("lock");
31 | headers.insert("Location".to_string(), redirect);
32 | headers.insert("Content-Length".to_string(), "0".to_string());
33 | }
34 | self
35 | }
36 |
37 | pub fn status(&mut self, status: u16) -> &mut Self {
38 | self.ctx
39 | .set_response_status
40 | .store(status, std::sync::atomic::Ordering::Relaxed);
41 | self
42 | }
43 |
44 | pub fn body(&mut self, body: Bytes) -> &mut Self {
45 | let body_len = body.len();
46 | self.body = Some(body);
47 | {
48 | let mut headers = self.ctx.add_response_header.write().expect("lock");
49 | headers.insert("Content-Length".to_string(), body_len.to_string());
50 | }
51 | self
52 | }
53 |
54 | pub fn body_json(&mut self, body: Value) -> pingora::Result<&mut Self> {
55 | let body_bytes = match serde_json::to_vec(&body) {
56 | Ok(b) => b,
57 | Err(e) => {
58 | return Err(pingora::Error::because(
59 | ErrorType::InternalError,
60 | "[Response]".to_string(),
61 | e.to_string(),
62 | ));
63 | }
64 | };
65 | self.body(Bytes::from(body_bytes));
66 | // self.header("Content-Type", "application/json");
67 | Ok(self)
68 | }
69 |
70 | pub async fn send(&mut self, session: &mut Session) -> pingora::Result {
71 | let status = self
72 | .ctx
73 | .set_response_status
74 | .load(std::sync::atomic::Ordering::Relaxed);
75 | let mut headers = ResponseHeader::build(status, None)?;
76 | self.proxy
77 | .response_filter(session, &mut headers, self.ctx)
78 | .await?;
79 | let mut tasks = vec![HttpTask::Header(Box::new(headers), false)];
80 | let _ = self
81 | .proxy
82 | .response_body_filter(session, &mut self.body, false, self.ctx)
83 | .is_ok();
84 | if let Some(body) = self.body.clone() {
85 | tasks.push(HttpTask::Body(Some(body), false));
86 | } else {
87 | tasks.push(HttpTask::Body(None, false));
88 | }
89 | tasks.push(HttpTask::Done);
90 | // println!("tasks: {:?}", tasks);
91 | if let Err(e) = session.response_duplex_vec(tasks).await {
92 | tracing::error!("Error sending response: {:?}", e);
93 | return Err(pingora::Error::because(
94 | ErrorType::InternalError,
95 | "[Response]".to_string(),
96 | e.to_string(),
97 | ));
98 | }
99 | Ok(true)
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Nylon Proxy Server Makefile
2 | #
3 | # This Makefile provides targets for building, developing, and managing the Nylon proxy server.
4 | # It includes development tools, build processes, and example compilation.
5 |
6 | # Configuration
7 | PORTS := 8088 8443 6192
8 | RUST_BACKTRACE := 1
9 | RUST_LOG := "info,warn,debug"
10 | DEV_CMD := cargo watch -w crates -w examples -w proto -w sdk -q -c -s "make build-examples && cargo run -- run --config ./examples/config.yaml"
11 |
12 | # Default target
13 | .PHONY: default
14 | default: dev
15 |
16 | # Development target - runs the server in development mode
17 | .PHONY: dev
18 | dev:
19 | @echo "🧹 Cleaning up existing processes on ports: $(PORTS)"
20 | @for port in $(PORTS); do \
21 | kill -9 $$(lsof -t -i :$$port) 2>/dev/null || true; \
22 | done
23 | @echo "🚀 Starting Nylon development server..."
24 | RUST_BACKTRACE=$(RUST_BACKTRACE) $(DEV_CMD)
25 |
26 | # Development target with debug logging
27 | .PHONY: dev-debug
28 | dev-debug:
29 | @echo "🐛 Starting Nylon development server with debug logging..."
30 | RUST_LOG=$(RUST_LOG) $(DEV_CMD)
31 |
32 | # Generate FlatBuffers code from protocol definitions
33 | .PHONY: generate
34 | generate:
35 | @echo "📝 Generating FlatBuffers code..."
36 | flatc --rust -o sdk/rust/src/fbs proto/plugin.fbs
37 | flatc --go -o sdk/go/fbs proto/plugin.fbs
38 | @echo "✅ FlatBuffers code generation completed"
39 |
40 | # Build Go examples
41 | .PHONY: build-examples
42 | build-examples:
43 | @echo "🔨 Building Go examples..."
44 | cd examples/go && go build -buildmode=c-shared -o ./../../target/examples/go/plugin_sdk.so
45 | @echo "✅ Go examples built successfully"
46 |
47 | # Build release version
48 | .PHONY: build
49 | build:
50 | @echo "🏗️ Building Nylon release version..."
51 | cargo build --release
52 | @echo "✅ Release build completed"
53 |
54 | # Clean build artifacts
55 | .PHONY: clean
56 | clean:
57 | @echo "🧹 Cleaning build artifacts..."
58 | cargo clean
59 | rm -rf target/examples/go/plugin_sdk.so
60 | @echo "✅ Clean completed"
61 |
62 | # Run tests
63 | .PHONY: test
64 | test:
65 | @echo "🧪 Running tests..."
66 | cargo test
67 | @echo "✅ Tests completed"
68 |
69 | # Check code formatting
70 | .PHONY: fmt
71 | fmt:
72 | @echo "🎨 Checking code formatting..."
73 | cargo fmt --check
74 | @echo "✅ Code formatting check completed"
75 |
76 | # Run clippy linter
77 | .PHONY: clippy
78 | clippy:
79 | @echo "🔍 Running clippy linter..."
80 | cargo clippy -- -D warnings
81 | @echo "✅ Clippy check completed"
82 |
83 | # Full code quality check
84 | .PHONY: check
85 | check: fmt clippy test
86 | @echo "✅ All code quality checks passed"
87 |
88 | # Install development dependencies
89 | .PHONY: install-dev
90 | install-dev:
91 | @echo "📦 Installing development dependencies..."
92 | cargo install cargo-watch
93 | @echo "✅ Development dependencies installed"
94 |
95 | # Show help
96 | .PHONY: help
97 | help:
98 | @echo "Nylon Proxy Server - Available targets:"
99 | @echo ""
100 | @echo " dev - Start development server with hot reload"
101 | @echo " dev-debug - Start development server with debug logging"
102 | @echo " build - Build release version"
103 | @echo " build-examples - Build Go plugin examples"
104 | @echo " generate - Generate FlatBuffers code"
105 | @echo " test - Run tests"
106 | @echo " fmt - Check code formatting"
107 | @echo " clippy - Run clippy linter"
108 | @echo " check - Run all code quality checks"
109 | @echo " clean - Clean build artifacts"
110 | @echo " install-dev - Install development dependencies"
111 | @echo " help - Show this help message"
112 |
--------------------------------------------------------------------------------
/crates/nylon/src/context.rs:
--------------------------------------------------------------------------------
1 | use async_trait::async_trait;
2 | use nylon_error::NylonError;
3 | use nylon_types::context::NylonContext;
4 | use pingora::proxy::Session;
5 | use std::sync::atomic::Ordering;
6 |
7 | #[async_trait]
8 | pub trait NylonContextExt {
9 | async fn parse_request(&self, session: &mut Session) -> Result<(), NylonError>;
10 | }
11 |
12 | #[async_trait]
13 | impl NylonContextExt for NylonContext {
14 | async fn parse_request(&self, session: &mut Session) -> Result<(), NylonError> {
15 | {
16 | let mut client_ip = self
17 | .client_ip
18 | .write()
19 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?;
20 | *client_ip = match session.client_addr() {
21 | Some(ip) => match ip.as_inet() {
22 | Some(ip) => ip.ip().to_string(),
23 | None => {
24 | return Err(NylonError::HttpException(
25 | 400,
26 | "BAD_REQUEST",
27 | "Unable to get client IP",
28 | ));
29 | }
30 | },
31 | None => {
32 | return Err(NylonError::HttpException(
33 | 400,
34 | "BAD_REQUEST",
35 | "Unable to get client IP",
36 | ));
37 | }
38 | };
39 | }
40 | let is_tls = match session.digest() {
41 | Some(d) => d.ssl_digest.is_some(),
42 | None => false,
43 | };
44 | self.tls.store(is_tls, Ordering::Relaxed);
45 | // reset per-request caches
46 | {
47 | if let Ok(mut q) = self.cached_query.write() {
48 | *q = None;
49 | }
50 | if let Ok(mut c) = self.cached_cookies.write() {
51 | *c = None;
52 | }
53 | }
54 | match session.as_http2() {
55 | Some(session) => {
56 | let host = session.req_header().uri.host().unwrap_or("");
57 | let mut h = self
58 | .host
59 | .write()
60 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?;
61 | *h = host.to_string();
62 | let mut p = self
63 | .port
64 | .write()
65 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?;
66 | *p = "".to_string();
67 | }
68 | None => {
69 | let host = match session.req_header().headers.get("Host") {
70 | Some(h) => {
71 | // h.to_str().unwrap_or("").split(':').next().unwrap_or("")
72 | let host = h.to_str().unwrap_or("").split(':').next().unwrap_or("");
73 | let port = h.to_str().unwrap_or("").split(':').nth(1).unwrap_or("");
74 | let mut p = self
75 | .port
76 | .write()
77 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?;
78 | *p = port.to_string();
79 | host
80 | }
81 | None => "",
82 | };
83 | let mut h = self
84 | .host
85 | .write()
86 | .map_err(|_| NylonError::InternalServerError("lock poisoned".into()))?;
87 | *h = host.to_string();
88 | }
89 | }
90 | Ok(())
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/docs/introduction/what-is-nylon.md:
--------------------------------------------------------------------------------
1 | # What is Nylon?
2 |
3 | Nylon is a high-performance HTTP/HTTPS reverse proxy built with Rust, powered by Cloudflare's Pingora framework. It's designed to be fast, reliable, and highly extensible through its plugin system.
4 |
5 | ## Why Nylon?
6 |
7 | ### ⚡️ Performance First
8 |
9 | Built on top of Pingora, the same technology that powers Cloudflare's edge network, Nylon delivers:
10 | - Low latency request handling
11 | - Efficient memory usage
12 | - High throughput
13 | - Connection pooling and reuse
14 |
15 | ### 🔌 Plugin Ecosystem
16 |
17 | Extend Nylon's functionality with plugins:
18 | - Request/Response filtering
19 | - Authentication and authorization
20 | - Custom logging and metrics
21 | - WebSocket message handling
22 | - Go SDK ready, more languages coming soon
23 |
24 | ### 🎯 Enterprise Features
25 |
26 | - **Multiple Load Balancing Strategies**: Round Robin, Weighted, Consistent Hashing, Random
27 | - **TLS/HTTPS Support**: Automatic certificate management with ACME (Let's Encrypt)
28 | - **Advanced Routing**: Path-based and host-based routing with parameter extraction
29 | - **Dynamic Configuration**: Hot-reload configuration without downtime
30 | - **Observability**: Comprehensive logging with request/response metrics
31 |
32 | ## Use Cases
33 |
34 | ### API Gateway
35 | Use Nylon as a centralized entry point for your microservices:
36 | - Route requests to appropriate services
37 | - Handle authentication and authorization
38 | - Rate limiting and throttling
39 | - Request/response transformation
40 |
41 | ### Load Balancer
42 | Distribute traffic across multiple backend servers:
43 | - Health checks
44 | - Connection pooling
45 | - Automatic failover
46 | - Session persistence
47 |
48 | ### WebSocket Proxy
49 | Proxy WebSocket connections with:
50 | - Message filtering and transformation
51 | - Room-based broadcasting
52 | - Connection management
53 |
54 | ## Architecture
55 |
56 | ```
57 | ┌─────────────┐
58 | │ Client │
59 | └──────┬──────┘
60 | │
61 | │ HTTP/HTTPS/WebSocket
62 | │
63 | ┌──────▼──────────────────────┐
64 | │ Nylon Proxy │
65 | │ ┌────────────────────────┐ │
66 | │ │ Plugin System │ │
67 | │ │ ┌──────────────────┐ │ │
68 | │ │ │ Request Filter │ │ │
69 | │ │ │ Response Filter │ │ │
70 | │ │ │ Body Filter │ │ │
71 | │ │ │ Logging │ │ │
72 | │ │ └──────────────────┘ │ │
73 | │ └────────────────────────┘ │
74 | │ ┌────────────────────────┐ │
75 | │ │ Routing Engine │ │
76 | │ └────────────────────────┘ │
77 | │ ┌────────────────────────┐ │
78 | │ │ Load Balancer │ │
79 | │ └────────────────────────┘ │
80 | └──────┬──────────────────────┘
81 | │
82 | │ Multiple strategies
83 | │
84 | ┌──────▼──────┐ ┌─────────────┐ ┌─────────────┐
85 | │ Backend 1 │ │ Backend 2 │ │ Backend 3 │
86 | └─────────────┘ └─────────────┘ └─────────────┘
87 | ```
88 |
89 | ## What Makes Nylon Different?
90 |
91 | - **🦀 Built with Rust on Pingora** - Leverages Cloudflare's battle-tested framework for unmatched performance and reliability
92 | - **🔌 Flexible Plugin System** - FFI-based architecture supporting multiple languages (Go SDK ready, more coming)
93 | - **⚡️ True Zero-Downtime** - Hot reload configuration and code without dropping a single connection
94 | - **🔒 Security First** - Automatic TLS with ACME, built-in security headers, and safe plugin isolation
95 | - **📊 Observable by Default** - Comprehensive logging, metrics, and health checks out of the box
96 | - **🎯 Developer Friendly** - Clean YAML config, intuitive APIs, and extensive documentation
97 |
98 | ## Next Steps
99 |
100 |
101 |
Ready to get started?
102 |
Check out the Quick Start guide to begin using Nylon.
103 |
104 |
105 |
--------------------------------------------------------------------------------
/crates/nylon-error/src/lib.rs:
--------------------------------------------------------------------------------
1 | use serde_json::{Value, json};
2 | use thiserror::Error;
3 |
4 | #[derive(Error, Debug, Clone)]
5 | pub enum NylonError {
6 | #[error("Failed to parse configuration: {0}")]
7 | ConfigError(String),
8 |
9 | #[error("Could not start the Pingora server: {0}")]
10 | PingoraError(String),
11 |
12 | #[error("Runtime error occurred: {0}")]
13 | RuntimeError(String),
14 |
15 | #[error("{{ \"status\": \"{0}\", \"error\": \"{1}\", \"message\": \"{2}\" }}")]
16 | HttpException(u16, &'static str, &'static str),
17 |
18 | #[error("Requested service is unavailable: {0}")]
19 | ServiceNotFound(String),
20 |
21 | #[error("No route matched the request: {0}")]
22 | RouteNotFound(String),
23 |
24 | #[error("Unable to generate ACME key pair: {0}")]
25 | AcmeKeyPairError(String),
26 |
27 | #[error("ACME HTTP request encountered an error: {0}")]
28 | AcmeHttpClientError(String),
29 |
30 | #[error("ACME JWS signing failed: {0}")]
31 | AcmeJWSError(String),
32 |
33 | #[error("ACME client encountered an error: {0}")]
34 | AcmeClientError(String),
35 |
36 | #[error("An unexpected internal server error occurred: {0}")]
37 | InternalServerError(String),
38 |
39 | #[error(
40 | "[BUG] This should never happen. Please report it at https://github.com/AssetsArt/nylon: {0}"
41 | )]
42 | ShouldNeverHappen(String),
43 | }
44 |
45 | impl NylonError {
46 | pub fn http_status(&self) -> u16 {
47 | match self {
48 | NylonError::HttpException(status, _, _) => *status,
49 | _ => 500,
50 | }
51 | }
52 |
53 | pub fn error_code(&self) -> String {
54 | match self {
55 | NylonError::HttpException(_, error, _) => error.to_string(),
56 | NylonError::ConfigError(_) => "CONFIG_ERROR".to_string(),
57 | NylonError::PingoraError(_) => "PINGORA_ERROR".to_string(),
58 | NylonError::RuntimeError(_) => "RUNTIME_ERROR".to_string(),
59 | NylonError::ServiceNotFound(_) => "SERVICE_NOT_FOUND".to_string(),
60 | NylonError::RouteNotFound(_) => "ROUTE_NOT_FOUND".to_string(),
61 | NylonError::AcmeKeyPairError(_) => "ACME_KEY_PAIR_ERROR".to_string(),
62 | NylonError::AcmeHttpClientError(_) => "ACME_HTTP_CLIENT_ERROR".to_string(),
63 | NylonError::AcmeJWSError(_) => "ACME_JWS_ERROR".to_string(),
64 | NylonError::AcmeClientError(_) => "ACME_CLIENT_ERROR".to_string(),
65 | NylonError::InternalServerError(_) => "INTERNAL_SERVER_ERROR".to_string(),
66 | NylonError::ShouldNeverHappen(_) => "SHOULD_NEVER_HAPPEN".to_string(),
67 | }
68 | }
69 |
70 | pub fn message(&self) -> String {
71 | match self {
72 | NylonError::HttpException(_, _, message) => message.to_string(),
73 | NylonError::ConfigError(message) => message.to_string(),
74 | NylonError::PingoraError(message) => message.to_string(),
75 | NylonError::RuntimeError(message) => message.to_string(),
76 | NylonError::ServiceNotFound(message) => message.to_string(),
77 | NylonError::RouteNotFound(message) => message.to_string(),
78 | NylonError::AcmeKeyPairError(message) => message.to_string(),
79 | NylonError::AcmeHttpClientError(message) => message.to_string(),
80 | NylonError::AcmeJWSError(message) => message.to_string(),
81 | NylonError::AcmeClientError(message) => message.to_string(),
82 | NylonError::InternalServerError(message) => message.to_string(),
83 | NylonError::ShouldNeverHappen(message) => format!(
84 | "[BUG] This should never happen. Please report it at https://github.com/AssetsArt/nylon: {}",
85 | message
86 | ),
87 | }
88 | }
89 |
90 | pub fn exception_json(&self) -> Value {
91 | json!({
92 | "status": self.http_status(),
93 | "error": self.error_code(),
94 | "message": self.message(),
95 | })
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/docs/examples/basic-proxy.md:
--------------------------------------------------------------------------------
1 | # Basic Proxy
2 |
3 | A simple reverse proxy example forwarding HTTP requests to a backend.
4 |
5 | ## Configuration
6 |
7 | ### Runtime Config (`config.yaml`)
8 |
9 | ```yaml
10 | http:
11 | - "0.0.0.0:8080"
12 |
13 | config_dir: "./config"
14 |
15 | pingora:
16 | daemon: false
17 | threads: 4
18 | work_stealing: true
19 | ```
20 |
21 | ### Proxy Config (`config/proxy.yaml`)
22 |
23 | ```yaml
24 | services:
25 | - name: backend
26 | service_type: http
27 | algorithm: round_robin
28 | endpoints:
29 | - ip: 127.0.0.1
30 | port: 3000
31 |
32 | routes:
33 | - route:
34 | type: host
35 | value: localhost
36 | name: default
37 | paths:
38 | - path:
39 | - /
40 | - /{*path}
41 | service:
42 | name: backend
43 | ```
44 |
45 | ## Running
46 |
47 | ```bash
48 | nylon run -c config.yaml
49 | ```
50 |
51 | ## Testing
52 |
53 | ```bash
54 | # Make a request
55 | curl http://localhost:8080
56 |
57 | # With headers
58 | curl -H "X-Custom-Header: value" http://localhost:8080/api
59 |
60 | # POST request
61 | curl -X POST -d '{"key":"value"}' \
62 | -H "Content-Type: application/json" \
63 | http://localhost:8080/api
64 | ```
65 |
66 | ## Multiple Backends
67 |
68 | Add more endpoints for load balancing:
69 |
70 | ```yaml
71 | services:
72 | - name: backend
73 | service_type: http
74 | algorithm: round_robin
75 | endpoints:
76 | - ip: 127.0.0.1
77 | port: 3000
78 | - ip: 127.0.0.1
79 | port: 3001
80 | - ip: 127.0.0.1
81 | port: 3002
82 | ```
83 |
84 | ## Health Checks
85 |
86 | Enable health checking:
87 |
88 | ```yaml
89 | services:
90 | - name: backend
91 | service_type: http
92 | algorithm: round_robin
93 | endpoints:
94 | - ip: 127.0.0.1
95 | port: 3000
96 | - ip: 127.0.0.1
97 | port: 3001
98 | health_check:
99 | enabled: true
100 | path: /health
101 | interval: 5s
102 | timeout: 2s
103 | healthy_threshold: 2
104 | unhealthy_threshold: 3
105 | ```
106 |
107 | ## Path-Based Routing
108 |
109 | Route different paths to different services:
110 |
111 | ```yaml
112 | services:
113 | - name: api-service
114 | service_type: http
115 | algorithm: round_robin
116 | endpoints:
117 | - ip: 127.0.0.1
118 | port: 3000
119 |
120 | - name: admin-service
121 | service_type: http
122 | algorithm: round_robin
123 | endpoints:
124 | - ip: 127.0.0.1
125 | port: 4000
126 |
127 | routes:
128 | - route:
129 | type: host
130 | value: localhost
131 | name: default
132 | paths:
133 | - path: /api/{*path}
134 | service:
135 | name: api-service
136 |
137 | - path: /admin/{*path}
138 | service:
139 | name: admin-service
140 | ```
141 |
142 | ## Host-Based Routing
143 |
144 | Route by hostname:
145 |
146 | ```yaml
147 | services:
148 | - name: app1-service
149 | service_type: http
150 | algorithm: round_robin
151 | endpoints:
152 | - ip: 127.0.0.1
153 | port: 3000
154 |
155 | - name: app2-service
156 | service_type: http
157 | algorithm: round_robin
158 | endpoints:
159 | - ip: 127.0.0.1
160 | port: 4000
161 |
162 | routes:
163 | - route:
164 | type: host
165 | value: app1.example.com
166 | name: app1
167 | paths:
168 | - path:
169 | - /
170 | - /{*path}
171 | service:
172 | name: app1-service
173 |
174 | - route:
175 | type: host
176 | value: app2.example.com
177 | name: app2
178 | paths:
179 | - path:
180 | - /
181 | - /{*path}
182 | service:
183 | name: app2-service
184 | ```
185 |
186 | ## Next Steps
187 |
188 | - [Authentication Example](/examples/authentication)
189 | - [Static Files](/core/configuration#static-service)
190 | - [Load Balancing](/core/configuration#services)
191 |
--------------------------------------------------------------------------------
/docs/plugins/go-sdk.md:
--------------------------------------------------------------------------------
1 | # Go SDK
2 |
3 | Complete Go SDK API reference. Pair this document with the [plugin overview](/plugins/overview) for end-to-end examples.
4 |
5 | ## Installation
6 |
7 | ```bash
8 | go get github.com/AssetsArt/nylon/sdk/go/sdk
9 | ```
10 |
11 | ## Basic Usage
12 |
13 | ```go
14 | package main
15 |
16 | import "C"
17 | import sdk "github.com/AssetsArt/nylon/sdk/go/sdk"
18 |
19 | func main() {}
20 |
21 | func init() {
22 | plugin := sdk.NewNylonPlugin()
23 |
24 | plugin.AddPhaseHandler("handler", func(phase *sdk.PhaseHandler) {
25 | phase.RequestFilter(func(ctx *sdk.PhaseRequestFilter) {
26 | // Gate, mutate, or short-circuit the request.
27 | ctx.Next()
28 | })
29 | })
30 | }
31 | ```
32 |
33 | ## API Reference
34 |
35 | ### Request helpers
36 |
37 | | Method | Description |
38 | |--------|-------------|
39 | | `req.Method()` | HTTP method (`GET`, `POST`, …). |
40 | | `req.Path()` | Request path. |
41 | | `req.URL()` | Full URL (scheme + host + path + query). |
42 | | `req.Query()` | Raw query string. |
43 | | `req.Params()` | Route parameters (`map[string]string`). |
44 | | `req.Header(name)` | Single header value. |
45 | | `req.Headers()` | Iterator with `.Get`/`.GetAll()` helpers. |
46 | | `req.RawBody()` | Request body (lazy-loaded). |
47 | | `req.Host()` | Host header. |
48 | | `req.ClientIP()` | Client IP address. |
49 | | `req.Timestamp()` | Request timestamp (milliseconds). |
50 | | `req.Bytes()` | Request body size. |
51 |
52 | ### Response helpers
53 |
54 | | Method | Description |
55 | |--------|-------------|
56 | | `res.SetStatus(code)` | Set status code. |
57 | | `res.Status()` | Retrieve status. |
58 | | `res.SetHeader(name, value)` | Set/overwrite header. |
59 | | `res.RemoveHeader(name)` | Remove header. |
60 | | `res.Headers()` | Map of response headers. |
61 | | `res.BodyRaw([]byte)` | Replace body with bytes. |
62 | | `res.BodyText(string)` | Convenience for UTF-8 text. |
63 | | `res.BodyJSON(any)` | Marshal and send JSON. |
64 | | `res.ReadBody()` | Read upstream body (response body filter / logging). |
65 | | `res.Redirect(url, code...)` | Issue redirect (default 302). |
66 | | `res.Bytes()` | Response size. |
67 | | `res.Duration()` | Elapsed time in ms. |
68 | | `res.Error()` | Captured upstream errors. |
69 | | `res.Stream()` | Start streaming response. |
70 |
71 | > `ctx.GetPayload()` is available on every phase context and returns the static payload configured in your YAML middleware entry.
72 |
73 | ## WebSocket helper APIs
74 |
75 | Upgrade an HTTP connection to WebSocket and register callbacks:
76 |
77 | ```go
78 | callbacks := sdk.WebSocketCallbacks{
79 | OnOpen: func(ws *sdk.WebSocketConn) {
80 | ws.JoinRoom("lobby")
81 | ws.SendText("Welcome!")
82 | },
83 | OnMessageText: func(ws *sdk.WebSocketConn, msg string) {
84 | ws.BroadcastText("lobby", msg)
85 | },
86 | OnClose: func(ws *sdk.WebSocketConn) {
87 | ws.LeaveRoom("lobby")
88 | },
89 | }
90 |
91 | if err := ctx.WebSocketUpgrade(callbacks); err != nil {
92 | res := ctx.Response()
93 | res.RemoveHeader("Content-Length")
94 | res.SetHeader("Transfer-Encoding", "chunked")
95 | res.SetStatus(400)
96 | res.BodyText("Upgrade failed")
97 | ctx.End()
98 | return
99 | }
100 |
101 | ctx.Next()
102 | ```
103 |
104 | Connection methods:
105 |
106 | | Method | Description |
107 | |--------|-------------|
108 | | `SendText(message)` | Send text frame. |
109 | | `SendBinary(data)` | Send binary frame. |
110 | | `Close()` | Close connection. |
111 | | `JoinRoom(room)` / `LeaveRoom(room)` | Convenience helpers for broadcast rooms. |
112 | | `BroadcastText(room, message)` | Broadcast text to room. |
113 | | `BroadcastBinary(room, data)` | Broadcast binary payload to room. |
114 |
115 | ## Streaming responses
116 |
117 | ```go
118 | stream, err := ctx.Response().Stream()
119 | if err != nil {
120 | log.Printf("stream error: %v", err)
121 | ctx.End()
122 | return
123 | }
124 |
125 | defer stream.End()
126 | stream.Write([]byte("chunk 1"))
127 | stream.Write([]byte("chunk 2"))
128 | ```
129 |
130 | ## Error handling patterns
131 |
132 | ```go
133 | if err := ctx.WebSocketUpgrade(callbacks); err != nil {
134 | res := ctx.Response()
135 | res.RemoveHeader("Content-Length")
136 | res.SetHeader("Transfer-Encoding", "chunked")
137 | res.SetStatus(400)
138 | res.BodyText("Error")
139 | ctx.End()
140 | return
141 | }
142 |
143 | ctx.Next()
144 | ```
145 |
146 | ## Best practices
147 |
148 | 1. **Always call `ctx.Next()`** unless you explicitly terminate the request with `ctx.End()`.
149 | 2. **Handle errors** – return proper status codes and chunked responses to avoid hangs.
150 | 3. **Avoid long blocking work** inside phase handlers; offload to goroutines if necessary.
151 | 4. **Use middleware payloads** for configuration; they are exposed through `ctx.GetPayload()` in every phase.
152 |
--------------------------------------------------------------------------------
/crates/nylon-plugin/src/loaders.rs:
--------------------------------------------------------------------------------
1 | use crate::constants::ffi_symbols;
2 | use dashmap::DashMap;
3 | use libloading::{Library, Symbol};
4 | use nylon_types::plugins::{
5 | FfiCloseSessionFn, FfiEventStreamFn, FfiInitializeFn, FfiPlugin, FfiPluginFreeFn,
6 | FfiRegisterSessionFn, FfiShutdownFn, PluginItem,
7 | };
8 | use std::sync::Arc;
9 |
10 | pub fn load(plugin: &PluginItem) {
11 | let file = plugin.file.clone();
12 | let lib_store =
13 | match nylon_store::get::>>(nylon_store::KEY_LIBRARY_FILE) {
14 | Some(lib) => lib,
15 | None => {
16 | let new_lib = DashMap::new();
17 | nylon_store::insert(nylon_store::KEY_LIBRARY_FILE, new_lib.clone());
18 | new_lib
19 | }
20 | };
21 |
22 | let lib = match lib_store.get(&file) {
23 | Some(lib) => lib,
24 | None => {
25 | let lib = unsafe {
26 | match Library::new(&file) {
27 | Ok(lib) => lib,
28 | Err(e) => {
29 | eprintln!("Failed to load shared library: {}", e);
30 | return;
31 | }
32 | }
33 | };
34 | lib_store.insert(file.clone(), Arc::new(lib));
35 | match lib_store.get(&file) {
36 | Some(lib) => lib,
37 | None => {
38 | eprintln!("Failed to get loaded library");
39 | return;
40 | }
41 | }
42 | }
43 | };
44 | let plugin_free = unsafe {
45 | let symbol: Symbol = lib
46 | .get(ffi_symbols::PLUGIN_FREE.as_bytes())
47 | .unwrap_or_else(|_| {
48 | panic!("Failed to load symbol: {}", ffi_symbols::PLUGIN_FREE);
49 | });
50 | std::mem::transmute::, Symbol<'static, FfiPluginFreeFn>>(symbol)
51 | };
52 | let register_session = unsafe {
53 | let symbol: Symbol = lib
54 | .get(ffi_symbols::REGISTER_SESSION.as_bytes())
55 | .unwrap_or_else(|_| {
56 | panic!("Failed to load symbol: {}", ffi_symbols::REGISTER_SESSION);
57 | });
58 | std::mem::transmute::, Symbol<'static, FfiRegisterSessionFn>>(
59 | symbol,
60 | )
61 | };
62 | let event_stream = unsafe {
63 | let symbol: Symbol = lib
64 | .get(ffi_symbols::EVENT_STREAM.as_bytes())
65 | .unwrap_or_else(|_| {
66 | panic!("Failed to load symbol: {}", ffi_symbols::EVENT_STREAM);
67 | });
68 | std::mem::transmute::, Symbol<'static, FfiEventStreamFn>>(symbol)
69 | };
70 | let close_session = unsafe {
71 | let symbol: Symbol = lib
72 | .get(ffi_symbols::CLOSE_SESSION.as_bytes())
73 | .unwrap_or_else(|_| {
74 | panic!("Failed to load symbol: {}", ffi_symbols::CLOSE_SESSION);
75 | });
76 | std::mem::transmute::, Symbol<'static, FfiCloseSessionFn>>(symbol)
77 | };
78 | let shutdown = unsafe {
79 | let symbol: Symbol = lib
80 | .get(ffi_symbols::SHUTDOWN.as_bytes())
81 | .unwrap_or_else(|_| {
82 | panic!("Failed to load symbol: {}", ffi_symbols::SHUTDOWN);
83 | });
84 | std::mem::transmute::, Symbol<'static, FfiShutdownFn>>(symbol)
85 | };
86 |
87 | let ffi_item = FfiPlugin {
88 | _lib: lib.clone(),
89 | plugin_free,
90 | register_session,
91 | event_stream,
92 | close_session,
93 | shutdown,
94 | };
95 | let plugins =
96 | match nylon_store::get::>>(nylon_store::KEY_PLUGINS) {
97 | Some(plugins) => plugins,
98 | None => {
99 | let new_plugins = DashMap::new();
100 | nylon_store::insert(nylon_store::KEY_PLUGINS, new_plugins.clone());
101 | new_plugins
102 | }
103 | };
104 | plugins.insert(plugin.name.clone(), Arc::new(ffi_item));
105 | nylon_store::insert(nylon_store::KEY_PLUGINS, plugins);
106 |
107 | // initialize
108 | let initialize = unsafe {
109 | let symbol: Symbol = lib
110 | .get(ffi_symbols::INITIALIZE.as_bytes())
111 | .unwrap_or_else(|_| {
112 | panic!("Failed to load symbol: {}", ffi_symbols::INITIALIZE);
113 | });
114 | std::mem::transmute::, Symbol<'static, FfiInitializeFn>>(symbol)
115 | };
116 | let config = match &plugin.config {
117 | Some(config) => serde_json::to_string(&config).unwrap_or_default(),
118 | None => "".to_string(),
119 | };
120 | let config_ptr = config.as_ptr();
121 | let config_len = config.len() as u32;
122 | unsafe {
123 | initialize(config_ptr, config_len);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/crates/nylon-types/src/context.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::type_complexity)]
2 |
3 | use crate::{plugins::SessionStream, route::MiddlewareItem, services::ServiceItem, template::Expr};
4 | use pingora::lb::Backend;
5 | use std::{
6 | collections::HashMap,
7 | sync::{
8 | RwLock,
9 | atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering},
10 | },
11 | };
12 |
13 | #[derive(Debug, Clone)]
14 | pub struct Route {
15 | pub service: ServiceItem,
16 | pub rewrite: Option,
17 | pub route_middleware: Option>>)>>,
18 | pub path_middleware: Option>>)>>,
19 | pub payload_ast: Option>>,
20 | }
21 |
22 | #[derive(Debug)]
23 | pub struct NylonContext {
24 | pub backend: RwLock,
25 | pub client_ip: RwLock,
26 | pub route: RwLock