# How to Modify Hysteria Core for Custom Features: Plugin Architecture Guide

> Customize Hysteria Core by leveraging its plugin architecture. Add custom features like authentication and routing without forking the repository. Explore the Go interfaces for seamless integration.

- Repository: [Aperture Internet Laboratory/hysteria](https://github.com/apernet/hysteria)
- Tags: how-to-guide
- Published: 2026-05-13

---

**Yes, Hysteria 2 exposes a plugin architecture via Go interfaces that lets you add custom authentication, routing, traffic accounting, and logging without forking the `apernet/hysteria` repository.**

Hysteria is a high-performance QUIC-based proxy tunnel. While the stock binary covers most deployment scenarios, the core library is deliberately modular. You can **modify Hysteria core** functionality by implementing Go interfaces defined in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go) and injecting them into the `Config` struct when constructing a server or client.

## Core Extension Points

The Hysteria server and client rely on interface-driven configuration. If you leave a field `nil`, the core falls back to safe defaults (`defaultOutbound`, no logging, etc.). To customize behavior, instantiate your own types that satisfy these contracts.

### Authentication (Authenticator)

The **Authenticator** interface controls whether a client is permitted to connect. It is defined in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go) and invoked during the initial QUIC handshake.

The method signature is:

```go
Authenticate(addr net.Addr, auth string, tx uint64) (bool, string)

```

Return `true` and a unique client identifier string to accept the connection, or `false` to reject. The `tx` parameter indicates transmit bytes at the time of auth, useful for replay protection.

### Request Filtering and Rewriting (RequestHook)

To inspect or mutate the first packet of a stream, implement the **RequestHook** interface. The server calls these methods in [`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go) (lines 66-84 for the auth flow and 190-203 for TCP handling).

The interface requires three methods:

- `Check(isUDP bool, reqAddr string) bool` – Return `false` to drop the request immediately.
- `TCP(stream server.HyStream, reqAddr *string) ([]byte, error)` – Inspect the stream and optionally rewrite `*reqAddr` before routing.
- `UDP(data []byte, reqAddr *string) error` – Same for UDP sessions.

### Outbound Connectivity (Outbound)

The **Outbound** interface lets you replace the default TCP and UDP dialers. This is useful for chaining proxies, custom DNS resolution, or traffic shaping. Defined in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go) (lines 52-59), it is instantiated inside `newH3sHandler` → `handleTCPRequest` in [`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go) (lines 90-105).

```go
type Outbound interface {
    TCP(reqAddr string) (net.Conn, error)
    UDP(reqAddr string) (server.UDPConn, error)
}

```

### Traffic Accounting and Quotas (TrafficLogger)

For per-client metrics or hard bandwidth limits, implement **TrafficLogger** from [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go) (lines 38-48). The core consults this interface during every read/write operation.

Key methods:

- `LogTraffic(id string, tx, rx uint64) bool` – Return `false` to force-disconnect the client (e.g., when a quota is exceeded).
- `LogOnlineState(id string, online bool)` – Track connect/disconnect events.
- `TraceStream(s server.HyStream, stats *server.StreamStats)` – Attach to specific streams for granular monitoring.

### Event Logging (EventLogger)

The **EventLogger** interface provides hooks for connection lifecycle events (`Connect`, `Disconnect`, `TCPRequest`, etc.), useful for audit trails or external alerting systems.

### Congestion and TLS Configuration

You can also **modify Hysteria core** transport behavior via `CongestionConfig` (selecting `bbr`, `cubic`, or `brutal` algorithms) and `TLSConfig`/`QUICConfig` structs to override certificates, receive windows, and idle timeouts.

## Practical Implementation Examples

### Custom RequestHook for Dynamic DNS Routing

This example rewrites the destination address based on a live DNS lookup, useful for geo-load balancing or bypassing censored DNS.

```go
// file: myhook.go
package myhook

import (
	"context"
	"net"
	
	"github.com/apernet/hysteria/core/v2/server"
)

type DNSHook struct{}

func (h *DNSHook) Check(isUDP bool, reqAddr string) bool { return true }

func (h *DNSHook) TCP(stream server.HyStream, reqAddr *string) ([]byte, error) {
	host, port, _ := net.SplitHostPort(*reqAddr)
	ips, err := net.DefaultResolver.LookupIP(context.Background(), "ip4", host)
	if err == nil && len(ips) > 0 {
		*reqAddr = net.JoinHostPort(ips[0].String(), port)
	}
	return nil, nil
}

func (h *DNSHook) UDP(data []byte, reqAddr *string) error { return nil }

```

Inject the hook when creating the server:

```go
cfg := &server.Config{
    // ... TLS and QUIC fields ...
    Authenticator: myAuth,
    RequestHook:   &myhook.DNSHook{},
}
srv, err := server.NewServer(cfg)
if err != nil {
    log.Fatal(err)
}
go srv.Serve()

```

### Custom Outbound for SOCKS5 Proxy Chaining

Replace the default direct dialer with a SOCKS5 proxy for all upstream connections.

```go
// file: socks5outbound.go
package socks5out

import (
	"net"
	
	"github.com/armon/go-socks5"
	"github.com/apernet/hysteria/core/v2/server"
)

type Socks5Outbound struct {
	Proxy *socks5.Server
}

func (o *Socks5Outbound) TCP(reqAddr string) (net.Conn, error) {
	return o.Proxy.Dial("tcp", reqAddr)
}

func (o *Socks5Outbound) UDP(reqAddr string) (server.UDPConn, error) {
	// Delegate to default for UDP, or implement custom logic
	return server.DefaultOutbound{}.UDP(reqAddr)
}

```

Wire it into the configuration:

```go
proxy, _ := socks5.New(&socks5.Config{})
cfg := &server.Config{
    Outbound: &socks5out.Socks5Outbound{Proxy: proxy},
}
srv, _ := server.NewServer(cfg)
_ = srv.Serve()

```

### Traffic Quota Enforcement with TrafficLogger

Enforce a hard limit per client (e.g., 10 MiB) by returning `false` from `LogTraffic` when the cap is reached.

```go
// file: quota_logger.go
package quota

import (
	"sync"
	"github.com/apernet/hysteria/core/v2/server"
)

type QuotaLogger struct {
	limit uint64
	mu    sync.Mutex
	used  map[string]uint64
}

func NewQuotaLogger(limit uint64) *QuotaLogger {
	return &QuotaLogger{limit: limit, used: make(map[string]uint64)}
}

func (q *QuotaLogger) LogTraffic(id string, tx, rx uint64) bool {
	q.mu.Lock()
	defer q.mu.Unlock()
	q.used[id] += tx + rx
	return q.used[id] <= q.limit // false triggers disconnect
}

func (q *QuotaLogger) LogOnlineState(id string, online bool) {}
func (q *QuotaLogger) TraceStream(s server.HyStream, _ *server.StreamStats) {}
func (q *QuotaLogger) UntraceStream(s server.HyStream) {}

```

Usage:

```go
cfg := &server.Config{
    TrafficLogger: quota.NewQuotaLogger(10 * 1024 * 1024), // 10 MiB
}
srv, _ := server.NewServer(cfg)
_ = srv.Serve()

```

## Client-Side Modifications

The client follows the same pattern. The **client.NewClient** function in [`core/client/client.go`](https://github.com/apernet/hysteria/blob/main/core/client/client.go) accepts a `*Config` defined in [`core/client/config.go`](https://github.com/apernet/hysteria/blob/main/core/client/config.go). While the client exposes fewer hooks than the server, you can still customize `TLSConfig`, `QUICConfig`, and `CongestionConfig` to control handshake parameters and congestion behavior on the dialer side.

## Summary

- Hysteria 2 uses a **plugin architecture** based on Go interfaces; you do not need to fork the repository to add **custom features**.
- Implement **Authenticator** for custom auth backends, **RequestHook** for packet inspection/rewriting, and **Outbound** for custom dialers.
- Use **TrafficLogger** to enforce quotas or export metrics; return `false` from `LogTraffic` to drop connections.
- All interfaces are defined in **[`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go)** and consumed in **[`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go)** and **[`core/client/client.go`](https://github.com/apernet/hysteria/blob/main/core/client/client.go)**.
- Pass your implementations via the `Config` struct to `server.NewServer()` or `client.NewClient()`.

## Frequently Asked Questions

### Do I need to fork the Hysteria repository to add custom features?

No. According to the `apernet/hysteria` source code, you can implement the extension interfaces (`Authenticator`, `RequestHook`, `Outbound`, `TrafficLogger`) in your own Go package, import the core modules, and inject your types via the `Config` struct. This keeps your custom logic separate from upstream updates.

### Which interface should I implement for per-client bandwidth quotas?

Implement the **TrafficLogger** interface defined in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go) (lines 38-48). The `LogTraffic(id string, tx, rx uint64) bool` method receives cumulative byte counts; returning `false` signals the core to terminate that client's connection immediately, effectively enforcing your quota limit.

### Can I modify both server and client behavior this way?

Yes. Both `server.NewServer()` and `client.NewClient()` accept a `Config` struct that exposes `TLSConfig`, `QUICConfig`, and `CongestionConfig`. However, the server side offers richer extension points (authentication hooks, traffic logging, request rewriting) compared to the client, which primarily allows transport-level customization.

### Where are the default implementations if I don't provide a custom plugin?

If you leave interface fields `nil` in the `Config` struct, [`core/server/server.go`](https://github.com/apernet/hysteria/blob/main/core/server/server.go) and [`core/client/client.go`](https://github.com/apernet/hysteria/blob/main/core/client/client.go) automatically instantiate safe defaults. For example, `defaultOutbound` in [`core/server/config.go`](https://github.com/apernet/hysteria/blob/main/core/server/config.go) (lines 68-84) provides standard TCP dialers and full-cone UDP listeners, ensuring the server or client remains functional without custom code.