add server/client tls certificates

This commit is contained in:
AndreyIgorevich 2025-07-11 04:42:49 +07:00
parent 54460c0f80
commit d04107ca8b
15 changed files with 143 additions and 199 deletions

View File

@ -7,8 +7,11 @@ GATEWAY_ROUTE_FILE_PATH=configs/routes.json
GATEWAY_GEO_DB_FILE_PATH=geodb/GeoLite2-Country.mmdb
### TLS SETTINGS
GATEWAY_TLS_FULLCHAIN_CERT=SSL/fullchain.pem
GATEWAY_TLS_PRIVATE_KEY=SSL/privkey.pem
GATEWAY_TLS_ROOT_CA=tls-certs/ca.crt
GATEWAY_TLS_CLIENT_CERT=tls-certs/client.crt
GATEWAY_TLS_CLIENT_KEY=tls-certs/client.key
GATEWAY_TLS_SERVER_CERT=tls-certs/service.crt
GATEWAY_TLS_SERVER_KEY=tls-certs/service.key
### LOG SETTINGS
GATEWAY_LOG_LEVEL=debug

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
.env
*.pem
*.sum
*.crt
*.key
*.mmdb

View File

@ -7,10 +7,7 @@ COPY go.sum ./
RUN go mod download
COPY . .
RUN openssl req -x509 -newkey rsa:4096 \
-nodes -keyout /app/SSL/privkey.pem \
-out /app/SSL/fullchain.pem -days 365 \
-subj "/CN=localhost"
RUN CGO_ENABLED=0 GOOS=linux go build -o gateway ./cmd/gateway/main.go
@ -21,7 +18,6 @@ RUN adduser -HD server
WORKDIR /app
COPY --from=builder /app/gateway /app/
COPY --from=builder /app/SSL /app/SSL
COPY ./configs /app/configs
COPY ./geodb /app/geodb
COPY ./.env /app/

138
README.md
View File

@ -1,138 +0,0 @@
# Yobble Gateway (Go)
## О проекте
Это высокопроизводительный API-шлюз, написанный на Go. Он служит единой точкой входа для всех клиентских запросов и направляет их в соответствующие микросервисы.
## Основные возможности
* **Динамическая маршрутизация:** Маршруты определяются в файле `configs/routes.json` и могут быть легко изменены без перезапуска шлюза.
* **Балансировка нагрузки:** Простая балансировка нагрузки (в настоящее время случайный выбор) между несколькими экземплярами сервиса.
* **SSL/TLS Termination:** Шлюз обрабатывает HTTPS-запросы, снимая нагрузку по шифрованию с внутренних сервисов.
* **Фильтрация по GeoIP:** Возможность блокировать запросы из определенных стран.
* **Конфигурация через переменные окружения:** Удобная настройка для различных сред (разработка, продакшн).
* **Логирование:** Структурированное логирование с использованием `slog`.
* **Graceful Shutdown:** Корректное завершение работы приложения, позволяющее завершить обработку текущих запросов.
## Технологический стек
* **Go (Golang)**
* **Gorilla Mux** для маршрутизации
* **maxmind/geoip2-golang** для GeoIP
* **YAML** и **JSON** для конфигурации
* **Docker** для контейнеризации
## Структура проекта
```
.
├── cmd/gateway/main.go # Точка входа в приложение
├── configs/ # Файлы конфигурации
│ ├── config.yml # Основной файл конфигурации
│ └── routes.json # Файл с маршрутами
├── internal/ # Внутренняя логика приложения
│ ├── config/ # Работа с конфигурацией
│ ├── logger/ # Логирование
│ ├── middleware/ # Промежуточное ПО (например, для получения Real IP)
│ ├── proxy/ # Логика проксирования и маршрутизации
│ └── server/ # Настройка и запуск HTTP-сервера
├── pkg/ # Пакеты, которые могут быть использованы в других проектах
│ └── geoip/ # Работа с GeoIP
├── geodb/ # База данных GeoIP
├── SSL/ # SSL-сертификаты
├── .env.example # Пример файла с переменными окружения
├── Dockerfile # Файл для сборки Docker-образа
└── go.mod # Зависимости проекта
```
## Быстрый старт
### 1. Клонирование репозитория
```bash
git clone <URL репозитория>
cd go-yobble-gateway
```
### 2. Конфигурация
Скопируйте файл с примером переменных окружения:
```bash
cp .env.example .env
```
Отредактируйте `.env` и `configs/config.yml` при необходимости.
### 3. Запуск с помощью Docker
Для сборки и запуска приложения в Docker-контейнере выполните:
```bash
docker-compose up --build
```
### 4. Локальный запуск (без Docker)
#### а) Установка зависимостей
```bash
go mod tidy
```
#### б) Запуск приложения
```bash
go run cmd/gateway/main.go --config=configs/config.yml
```
## Конфигурация
### Основная конфигурация (`config.yml`)
* `server`: Настройки хоста и порта сервера.
* `gateway`: Настройки шлюза, включая путь к файлу с маршрутами и базу GeoIP.
* `tls`: Пути к SSL-сертификатам.
* `logging`: Уровень и формат логирования.
### Переменные окружения (`.env`)
Переменные окружения, определенные в `.env`, переопределяют значения по умолчанию в `config.yml`.
### Маршрутизация (`routes.json`)
Файл `routes.json` определяет, как шлюз будет перенаправлять запросы.
**Пример:**
```json
{
"v1": {
"/auth": [
{
"url": "https://auth-service:5201",
"role": "master",
"allow_self_signed": true
}
]
},
"default": {
"/docs": [
{
"url": "https://docs-service:5199",
"role": "slave",
"allow_self_signed": true
}
]
}
}
```
* **Ключи верхнего уровня (`v1`, `default`)** - это версии API. Запросы, начинающиеся с `/v1/...`, будут сопоставляться с маршрутами в секции `"v1"`. Если версия не указана, используется секция `"default"`.
* **Ключи второго уровня (`/auth`, `/docs`)** - это префиксы путей. Шлюз ищет наиболее длинное совпадение префикса.
* **`url`** - URL внутреннего сервиса.
* **`allow_self_signed`** - Разрешает использование самоподписанных сертификатов для внутреннего сервиса.
## GeoIP Фильтрация
Шлюз может блокировать доступ для стран, перечисленных в `blocked_countries` в файле `config.yml`. Для работы этой функции необходима база данных `GeoLite2-Country.mmdb` от MaxMind.

View File

@ -24,25 +24,24 @@ func main() {
configPath = os.Getenv("CONFIG_PATH")
}
logger.InitLogger("info", "text")
op := "main.main"
log := logger.NewLoggerWithOp(op)
cfg, err := config.Load(configPath)
cfg, err := config.Load(log, configPath)
if err != nil {
log.Error("failed to load configuration", slog.String("path", configPath), slog.Any("error", err))
os.Exit(1)
}
// Повторно инициализируем логгер с настройками из конфигурации
logger.InitLogger(cfg.Logging.Level, cfg.Logging.Format)
log = logger.NewLoggerWithOp(op) // Повторно создаем логгер с обновленным уровнем и форматом
logger.InitLogger(&cfg.Logger)
log = logger.NewLoggerWithOp(op)
log.Info("configuration loaded successfully")
log.Debug("ROUTE CFG: ",
slog.Any("routes", cfg.RouteConfig))
// Инициализируем сервис GeoIP
geoipService, err := geoip.NewGeoIPService(cfg.Gateway.GeoIPDB, cfg.Gateway.BlockedCountries)
geoipService, err := geoip.NewGeoIPService(&cfg.Gateway)
if err != nil {
log.Error("failed to initialize GeoIP service", slog.Any("error", err))
os.Exit(1)
@ -54,7 +53,7 @@ func main() {
// Запускаем сервер в отдельной горутине
done := make(chan struct{})
errChan := srv.Start(cfg.TLS.FullChain, cfg.TLS.PrivateKey)
errChan := srv.Start(&cfg.TLS)
// Настраиваем перехват системных сигналов для graceful shutdown
sigChan := make(chan os.Signal, 1)

View File

@ -1,18 +1,21 @@
server:
host: ${SERVER_HOST:0.0.0.0}
port: ${SERVER_PORT:5150}
host: ${GATEWAY_SERVER_HOST:0.0.0.0}
port: ${GATEWAY_SERVER_PORT:5150}
gateway:
debug: false
route_file: ${ROUTE_FILE_PATH:configs/routes.json}
geoip_db: ${GEO_DB_FILE_PATH:geodb/GeoLite2-Country.mmdb}
route_file: ${GATEWAY_ROUTE_FILE_PATH:configs/routes.json}
geoip_db: ${GATEWAY_GEO_DB_FILE_PATH:geodb/GeoLite2-Country.mmdb}
blocked_countries:
- US
tls:
full_chain: ${TLS_FULLCHAIN_CERT:SSL/fullchain.pem}
private_key: ${TLS_PRIVATE_KEY:SSL/privkey.pem}
root_ca: ${GATEWAY_TLS_ROOT_CA:tls-certs/ca.crt}
client_cert: ${GATEWAY_TLS_CLIENT_CERT:tls-certs/client.crt}
client_key: ${GATEWAY_TLS_CLIENT_KEY:tls-certs/client.key}
server_cert: ${GATEWAY_TLS_SERVER_CERT:tls-certs/service.crt}
server_key: ${GATEWAY_TLS_SERVER_KEY:tls-certs/service.key}
logging:
level: ${LOG_LEVEL:debug} # debug, info, warn, error
format: ${LOG_FORMAT:text} # text, json
level: ${GATEWAY_LOG_LEVEL:info} # debug, info, warn, error
format: ${GATEWAY_LOG_FORMAT:text} # text, json

View File

@ -2,37 +2,42 @@
"v1": {
"/auth": [
{
"url": "https://0.0.0.0:5201",
"url": "https://yobble-auth-service:5201",
"role": "master",
"allow_self_signed": true
"yobble_signed": true,
"allow_untrusted": false
}
],
"/user": [
{
"url": "https://yobble-user-service:5202",
"role": "master",
"allow_self_signed": true
"yobble_signed": true,
"allow_untrusted": false
}
],
"/profile": [
{
"url": "https://yobble-profile-service:5203",
"role": "master",
"allow_self_signed": true
"yobble_signed": true,
"allow_untrusted": false
}
],
"/feed": [
{
"url": "https://yobble-feed-service:5204",
"role": "master",
"allow_self_signed": true
"yobble_signed": true,
"allow_untrusted": false
}
],
"/chat/private": [
{
"url": "https://localhost:5205",
"role": "master",
"allow_self_signed": true
"yobble_signed": true,
"allow_untrusted": false
}
]
},
@ -41,7 +46,8 @@
{
"url": "http://auth_v2_service:5301",
"role": "master",
"allow_self_signed": false
"yobble_signed": true,
"allow_untrusted": false
}
]
},
@ -50,14 +56,16 @@
{
"url": "https://yobble-docs-service:5199",
"role": "slave",
"allow_self_signed": true
"yobble_signed": true,
"allow_untrusted": false
}
],
"/test": [
{
"url": "https://localhost:9097",
"role": "test",
"allow_self_signed": true
"yobble_signed": true,
"allow_untrusted": false
}
]
}

View File

@ -7,7 +7,7 @@ services:
env_file:
- .env
# ПИТОНИСТ ЧТОБЫ ПРОКИНУТЬ SSL или иные конфиги примонтируй соответствующие volume
# volumes:
# - ./configs:/app/configs
volumes:
- ./tls-certs:/app/tls-certs
# - ./geodb:/app/geodb
# - ./SSL:/app/SSL

24
go.sum Normal file
View File

@ -0,0 +1,24 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -3,19 +3,18 @@ package config
import (
"encoding/json"
"github.com/joho/godotenv"
"gopkg.in/yaml.v3"
"log/slog"
"os"
"regexp"
"yobble-gateway-go/internal/logger"
"gopkg.in/yaml.v3"
)
// Backend представляет собой отдельный сервер бэкенда.
type Backend struct {
URL string `json:"url"`
Role string `json:"role"`
AllowSelfSigned bool `json:"allow_self_signed"`
URL string `json:"url"`
Role string `json:"role"`
YobbleSigned bool `json:"yobble_signed"`
AllowUntrusted bool `json:"allow_untrusted"`
}
// Routes сопоставляет строки версий с префиксами маршрутов и бэкендами.
@ -36,8 +35,11 @@ type GatewayConfig struct {
// TLSConfig содержит настройки, связанные с TLS.
type TLSConfig struct {
FullChain string `yaml:"full_chain"`
PrivateKey string `yaml:"private_key"`
RootCA string `yaml:"root_ca"`
ClientCert string `yaml:"client_cert"`
ClientKey string `yaml:"client_key"`
ServerCert string `yaml:"server_cert"`
ServerKey string `yaml:"server_key"`
}
// LogConfig содержит настройки, связанные с логированием.
@ -51,15 +53,15 @@ type Settings struct {
Server ServerConfig `yaml:"server"`
Gateway GatewayConfig `yaml:"gateway"`
TLS TLSConfig `yaml:"tls"`
Logging LogConfig `yaml:"logging"`
Logger LogConfig `yaml:"logging"`
RouteConfig Routes `yaml:"-"` // Загружается отдельно, не из корня YAML
}
// Load возвращает новый объект конфигурации, считывая YAML-файл.
func Load(configPath string) (*Settings, error) {
op := "config.Load"
log := logger.NewLoggerWithOp(op)
func Load(log *slog.Logger, configPath string) (*Settings, error) {
const op = "config.Load"
log = log.With(slog.String("op", op))
if err := godotenv.Load(); err != nil {
log.Warn("[Attention]: No .env file found or failed to load, using STANDARD settings!")

View File

@ -4,6 +4,7 @@ import (
"log/slog"
"os"
"strings"
"yobble-gateway-go/internal/config"
"github.com/lmittmann/tint"
)
@ -11,9 +12,9 @@ import (
var defaultLogger *slog.Logger
// InitLogger инициализирует логгер slog по умолчанию на основе предоставленного уровня и формата.
func InitLogger(levelStr, format string) *slog.Logger {
func InitLogger(cfg *config.LogConfig) *slog.Logger {
var level slog.Level
switch strings.ToLower(levelStr) {
switch strings.ToLower(cfg.Level) {
case "debug":
level = slog.LevelDebug
case "info":
@ -29,7 +30,7 @@ func InitLogger(levelStr, format string) *slog.Logger {
var handler slog.Handler
output := os.Stdout
switch strings.ToLower(format) {
switch strings.ToLower(cfg.Format) {
case "json":
handler = slog.NewJSONHandler(output, &slog.HandlerOptions{
Level: level,
@ -57,7 +58,11 @@ func InitLogger(levelStr, format string) *slog.Logger {
func NewLoggerWithOp(op string) *slog.Logger {
if defaultLogger == nil {
// Fallback if InitLogger was not called, though it should be.
defaultLogger = InitLogger("info", "text")
logConfig := config.LogConfig{
Level: "info",
Format: "text",
}
defaultLogger = InitLogger(&logConfig)
}
return defaultLogger.With(slog.String("op", op))
}

View File

@ -2,10 +2,13 @@ package proxy
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log/slog"
"net/http"
"net/http/httputil"
"net/url"
"os"
"yobble-gateway-go/internal/config"
"yobble-gateway-go/internal/logger"
@ -21,6 +24,7 @@ func NewProxyHandler(cfg *config.Settings, geoIPService *geoip.GeoIPService) htt
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Проверка GeoIP
realIP := middleware.GetRealIP(r)
log.Debug("real IP", slog.String("ip", realIP))
if realIP != "" {
isBlocked, countryCode := geoIPService.IsCountryBlocked(realIP)
if isBlocked {
@ -73,10 +77,21 @@ func NewProxyHandler(cfg *config.Settings, geoIPService *geoip.GeoIPService) htt
return nil
}
// Пользовательский транспорт для обработки allow_self_signed
if backend.AllowSelfSigned {
if backend.YobbleSigned {
tlsConfig, err := newTLSConfig(&cfg.TLS, backendURL.Host, backend.AllowUntrusted)
if err != nil {
log.Error("failed to create TLS config", slog.Any("error", err))
http.Error(w, "Upstream error", http.StatusBadGateway)
return
}
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
TLSClientConfig: tlsConfig,
}
} else if backend.AllowUntrusted {
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
@ -84,3 +99,27 @@ func NewProxyHandler(cfg *config.Settings, geoIPService *geoip.GeoIPService) htt
proxy.ServeHTTP(w, r)
})
}
func newTLSConfig(cfg *config.TLSConfig, serverName string, allowUntrusted bool) (*tls.Config, error) {
clientCert, err := tls.LoadX509KeyPair(cfg.ClientCert, cfg.ClientKey)
if err != nil {
return nil, fmt.Errorf("failed to load client certificate: %w", err)
}
caCert, err := os.ReadFile(cfg.RootCA)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return nil, fmt.Errorf("failed to append CA certificate to pool")
}
return &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
ServerName: serverName,
InsecureSkipVerify: allowUntrusted,
}, nil
}

View File

@ -57,14 +57,14 @@ func NewServer(cfg *config.Settings, geoIPService *geoip.GeoIPService) *Server {
// Start запускает HTTP/HTTPS сервер в отдельной горутине.
// Он возвращает канал, который будет закрыт, когда сервер завершит работу.
func (s *Server) Start(fullchain, privkey string) <-chan error {
func (s *Server) Start(cfg *config.TLSConfig) <-chan error {
errChan := make(chan error, 1)
s.log.Info("server starting with TLS and HTTP/2", slog.String("address", s.httpServer.Addr))
go func() {
defer close(errChan)
if err := s.httpServer.ListenAndServeTLS(fullchain, privkey); err != nil && !errors.Is(err, http.ErrServerClosed) {
if err := s.httpServer.ListenAndServeTLS(cfg.ServerCert, cfg.ServerKey); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.log.Error("server failed to listen and serve", slog.Any("error", err))
errChan <- err
}

View File

@ -3,6 +3,7 @@ package geoip
import (
"log/slog"
"net"
"yobble-gateway-go/internal/config"
"yobble-gateway-go/internal/logger"
"github.com/oschwald/geoip2-golang"
@ -15,22 +16,22 @@ type GeoIPService struct {
}
// NewGeoIPService создает новый GeoIPService.
func NewGeoIPService(dbPath string, blockedCountries []string) (*GeoIPService, error) {
func NewGeoIPService(cfg *config.GatewayConfig) (*GeoIPService, error) {
op := "geoip.NewGeoIPService"
log := logger.NewLoggerWithOp(op)
reader, err := geoip2.Open(dbPath)
reader, err := geoip2.Open(cfg.GeoIPDB)
if err != nil {
log.Error("failed to open GeoIP database", slog.String("db_path", dbPath), slog.Any("error", err))
log.Error("failed to open GeoIP database", slog.String("db_path", cfg.GeoIPDB), slog.Any("error", err))
return nil, err
}
blockedMap := make(map[string]struct{})
for _, country := range blockedCountries {
for _, country := range cfg.BlockedCountries {
blockedMap[country] = struct{}{}
}
log.Info("GeoIP service initialized", slog.String("db_path", dbPath), slog.Any("blocked_countries", blockedCountries))
log.Info("GeoIP service initialized", slog.String("db_path", cfg.GeoIPDB), slog.Any("blocked_countries", cfg.BlockedCountries))
return &GeoIPService{
reader: reader,
blockedCountries: blockedMap,