2019-02-03

О Go

Поковырялся я в Go. Который Golang от Google.
Подвернулся хипстерский стартаперский «legacy» проект, где «backend» был написан на Go. Так себе написан. Echo, конечно, неплохой веб фреймворк. Но он примерно как Flask. То есть маршруты, биндинги, JSONы, middleware и прочие веб прелести присутствуют. Для микросервисов вроде достаточно. Но это лишь сторона, обращённая к веб. А с другой стороны: клиенты к другим сервисам, базам данных и прочим — берите и изобретайте сами.
Простота написания первых хэндлеров способствует тому, что начинающие джуниоры начитают писать в «стиле PHP». В каждой хэндлер-функции всё вперемешку: приём данных, валидация, несколько SQL запросов, записанных прямо тут же, формирование ответа, выдача ошибок. И таких функций — полсотни уже. Дублирование кода? А что плохого? SOLID? Не, не слышали.
В общем, проблемы этого проекта не в языке. Но хочется поговорить именно о Go. В первый раз плотно с ним столкнулся. И... поражён.
Gopher
На localhost Go тоталитарен. Всё, что имеет отношение к Go, должно быть в $GOPATH. Это обязательная переменная окружения, которая указывает на некий каталог. Обычно на ~/go. И в $GOPATH/src должны лежать все ваши исходники, а в $GOPATH/bin будут складываться результаты компиляции. Ну ладно, сам компилятор может быть просто установлен в системе: /usr/bin/go. Если у вас несколько проектов, которые не должны пересекаться по исходникам, вам придётся для каждого завести свой $GOPATH и не забывать его переключать.
В Go нет библиотек. В Go нет линковки. Никаких бинарников, или архивов, или заголовочных файлов, которые можно подключить к проекту. Никакого контроля версий. Всё просто. Компилятор Go просто собирает некое подмножество *.go файлов из $GOPATH/src в один бинарник, который кладёт в $GOPATH/bin.
В $GOPATH/src каждый каталог представляет собой пакет (package). Пакет — это сколько угодно файлов .go в одном каталоге. Каждый файл должен начинаться со строки: package package_name.
Пакет с именем "main" — особенный. Он представляет собой программу, бинарник, который нужно собрать. Пакетов с именем "main" в $GOPATH/src может быть несколько, в разных каталогах. Значит, из этого дерева исходников можно собрать несколько бинарников.
Давайте создадим файл $GOPATH/src/echo_sample/main.go.
package main

func main() {
}
Это — минимальная программа на Go. Она ничего не делает. Но её можно скомпилировать и запустить.
$ cd $GOPATH/src/echo_sample
$ go install
$ $GOPATH/bin/echo_sample
Нехилый бинарничек на мегабайт получился.
$ ls -lh $GOPATH/bin
total 1.1M
-rwxrwxr-x 1 gelin gelin 1.1M Feb  2 15:40 echo_sample
Это потому что в Go нет динамических библиотек. Всё компилируется в один бинарник. Вон, в Terraform плагины — это отдельные сервера, с которыми главная программа общается по HTTP.
Но мы хотим веб сервер. Поэтому импортируем Echo.
import (
    "github.com/labstack/echo"
)
Пока у нас нет исходников Echo для компиляции. Но их легко получить.
$ cd $GOPATH/src/echo_sample
$ go get
# echo_sample
./main.go:4:2: imported and not used: "github.com/labstack/echo"
Команда go get скачала, внезапно, с самого GitHub, все исходники Echo, и все его зависимости.
$ tree -L 3 $GOPATH/src
/home/gelin/work/bitbucket/blog/go/go/src
├── echo_sample
│   └── main.go
├── github.com
│   ├── labstack
│   │   ├── echo
│   │   └── gommon
│   ├── mattn
│   │   ├── go-colorable
│   │   └── go-isatty
│   └── valyala
│       └── fasttemplate
└── golang.org
    └── x
        └── crypto

13 directories, 1 file
Причём это не просто исходники, а полноценные склонированные Git репозитории. Можно пулреквестить прямо оттуда :)
$ cd $GOPATH/src/github.com/labstack/echo
$ ls -a
.        bind_test.go     echo.go        _fixture        .github     go.sum         LICENSE   middleware   response_test.go  .travis.yml
..       context.go       echo_test.go   .git            .gitignore  group.go       log.go    README.md    router.go
bind.go  context_test.go  .editorconfig  .gitattributes  go.mod      group_test.go  Makefile  response.go  router_test.go
$ git status
On branch master
Your branch is up to date with 'origin/master'.

nothing to commit, working tree clean
$ git remote -v
origin  https://github.com/labstack/echo (fetch)
origin  https://github.com/labstack/echo (push)
Получается, что $GOPATH/src — это множество вложенных репозиториев. Из которых всё и собирается. И, само собой, берётся свежий master. Не очень хорошо для повторяемой сборки. Сам по себе Go не позволяет задать конкретную версию зависимостей.
Но в Go, начиная с версии 1.5, то есть уже довольно давно, есть поддержка вендоринга. Идея в том, что на уровне любого каталога в $GOPATH/src можно создать подкаталог vendor. А внутри него можно воссоздать часть дерева исходников. И это вендорное поддерево будет переопределять аналогичные пакеты в основном дереве.
Подкаталог vendor находится в репозитории данного пакета и подконтролен ему. Можно туда положить нужную правильную версию библиотеки. А для пущего удобства и чтобы не держать в своём репозитории полную копию исходников, можно воспользоваться инструментами управления вендорингом. Например, govendor.
$ cd $GOPATH/src/echo_sample
$ govendor init
$ govendor add +e
$ govendor list
 v  github.com/labstack/echo
 v  github.com/labstack/gommon/color
 v  github.com/labstack/gommon/log
 v  github.com/mattn/go-colorable
 v  github.com/mattn/go-isatty
 v  github.com/valyala/bytebufferpool
 v  github.com/valyala/fasttemplate
 v  golang.org/x/crypto/acme
 v  golang.org/x/crypto/acme/autocert
pl  echo_sample
  m golang.org/x/sys/unix
$ ls
main.go  vendor
$ ls vendor
github.com  golang.org  vendor.json
$ head vendor/vendor.json
{
        "comment": "",
        "ignore": "test",
        "package": [
                {
                        "checksumSHA1": "AlL1am5Xe3ouiyhL8OLlTDSUkG8=",
                        "path": "github.com/labstack/echo",
                        "revision": "bc28fceaf38c6ca5ce7d749c22bbd2ac0405170c",
                        "revisionTime": "2019-02-01T14:33:56Z"
                },
Зависимости помещаются в каталог vendor. Конкретные версии зависимостей, с точностью до ревизии в Git репозитории, записываются в файл vendor.json. Это похоже на package.json в мире NPM. Достаточно иметь файл vendor.json, а нужные зависимости в нужных ревизиях потом можно восстановить с помощью govendor.
С зависимостями разобрались. Пора делать сервер.
package main

import (
    "fmt"
    "github.com/labstack/echo"
    "net/http"
)

func main() {
    e := echo.New()
    e.GET("/", helloHandler)
    _ = e.Start(":8080")
}

func helloHandler(c echo.Context) error {
    name := c.QueryParam("name")
    message := fmt.Sprintf("Hello, %s!", name)
    return c.JSON(http.StatusOK, echo.Map{
        "message": message,
    })
}
Нам понадобится импортировать стандартные пакеты fmt и net/http. В первом — уйма функций форматирования вывода, в стиле сишных *printf, но чуть мощнее, и форматированного ввода. Во втором — стандартный HTTP клиент, нам оттуда нужны коды HTTP ответов, просто для красоты.
Проимпортированный пакет "github.com/labstack/echo" доступен по имени echo. Потому что там в файлах написано package echo. Особых конфликтов имён при импорте не возникает, потому что можно проимпортировать и под другим именем. Ну а длинное имя, что в импорте в кавычках писать, — это уникальный адрес репозитория.
В пакете у нас есть функция echo.New(). Она возвращает тип Echo. Точнее, с указанием пакета, echo.Echo. Точнее, указатель на него: *echo.Echo.
Для сохранения указателя в локальную переменную e используется операция :=. Она делает сразу три вещи: определяет переменную, выводит тип переменной, присваивает значение переменной. Так можно только для локальных переменных. И эта магия используется в Go повсеместно.
Все эти записи эквивалентны:
// 1
var e *echo.Echo
e = echo.New()
// 2
var e = echo.New()
// 3
e := echo.New()
Как видите, в Go типы указываются как в Pascal, или Kotlin, или Scala — после имени переменной, пусть и без двоеточия.
Типы. Всё странно с ними в Go.
Есть примитивные типы. Целые числа разных длин и значности: int8, int16, int32, int64, uint8, uint16, uint32, uint64. А есть и просто int, реальная длина которого зависит от платформы, как в C. Но нельзя просто так взять и присвоить один int другому. Всегда требуется явная конвертация значения. Это касается и просто int, который вроде на 64-битной платформе должен полностью совпадать с int64.
var i int
var i64 int64
i64 = 0x7FFFFFFFFFFFFFFF
i = i64     // cannot use i64 (type int64) as type int in assignment
i = int(i64)    // works
Литералы в Go не имеют типа. Компилятор сам генерирует значение нужной длины и типа, в зависимости от того, какой переменной его нужно присвоить. Поэтому не нужно писать 1L или 1.0, достаточно написать 1.
Вещественные числа: float32 и float64. Тут явно нужно разрядность указывать.
Есть комплексные числа: complex64 и complex128, соответственно, из двух float32 или float64.
Есть bool, который принимает значения true или false. Естественно, только bool может быть результатом проверки в if.
Есть string. Это встроенный тип. Строка может содержать любые символы Unicode. Фактически хранится массив байт в UTF-8. Строки неизменяемы. Строка считается примитивным типом и всегда передаётся по значению. Раз это вполне себе значение, строка не может быть nil. Тем не менее, у строки, так же как у всех встроенных типов, есть «нулевое» значение по умолчанию — пустая строка ("").
Указатели. Они есть. Они нужны, если нужно передать значение по ссылке. Или чтобы представить nil, так записывается местный null.
Для доступа к полям структуры или к методам указатели не нужно разыменовывать. Операция . (точка) прекрасно работает как непосредственно со значениями, так и с указателями. Так что, указатель это или нет, становится важно только при передаче значения в функцию или из функции.
Структуры. Самый часто используемый тип данных. Типа классы в Go. Ну по аналогии с тем, как структуры C стали классами C++. В структурах задаются поля. Всех возможных типов.
Например, тот же echo.Echo — это структура.
type (
    // Echo is the top-level framework instance.
    Echo struct {
        StdLogger        *stdLog.Logger
        colorer          *color.Color
        premiddleware    []MiddlewareFunc
        middleware       []MiddlewareFunc
        maxParam         *int
        router           *Router
// ...
На структуры, а на самом деле, на любые именованные типы тоже, можно навесить методы.
// GET registers a new GET route for a path with matching handler in the router
// with optional route-level middleware.
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
    return e.Add(http.MethodGet, path, h, m...)
}
Получатель (receiver) явно указывается в скобках перед именем метода. И ему вовсе не обязательно называться this. Получателем может быть и указатель. Если у вас переменная имеет тип-значение, то к ней можно применить все методы, навешанные на это значение. Если же у вас переменная-указатель, то можно вызвать методы, навешанные и на тип-значение, и на тип-указатель.
Этот receiver — не синтаксический сахар. Это не другой способ указать первый параметр функции. Это не extension функции, как в Kotlin или C#. Это действительно методы. Просто их можно определить в другом файле, примерно как в C++. Но в том же пакете.
Имена методов в Go часто начинаются с большой буквы. Это не следование соглашениям C#. Это — права доступа. Все объявления пакета: переменные, функции, структуры, поля структур — будут видны из других пакетов только если их имена начинаются с большой буквы. Всё, что с маленькой буквы, — приватно для этого пакета. Не сильно страшное правило, жалко, не работает для китайского языка. Чаще всего вам придётся работать со структурами и функциями других пакетов, они публичные, поэтому — с большой буквы.
На поля структуры можно навесить теги. Технически это строковый литерал. Но он сам по себе имеет определённый синтаксис, куда входят двойные кавычки. Поэтому само объявление тега обычно заключают в обратные кавычки, в которых возможно использовать разные страшные символы, даже перевод строки.
type Result struct {
    Success bool `json:"success"`
    Message string `json:"message"`
}
Теги в структурах часто используются для сериализации, десериализации и прочих маппингов. Их можно прочитать через рефлекшен. А так как сериализация в тот же JSON — это сторонний пакет, то структуры и поля, подлежащие сериализации, нужно объявлять с именами с большой буквы.
Да, в Go есть рефлекшен. И он частенько используется. Хотя бы для компенсации отсутствия generics.
Поля одной структуры можно включить в другую структуру. Некая разновидность композиции. При этом поля и методы вложенной структуры будут напрямую доступны и при обращении ко внешней структуре.
type Result struct {
    Success bool `json:"success"`
}

type MessageResult struct {
    Result
    Message string `json:"message"`
}
И это никоим образом не наследование. В Go вообще нет наследования. И, соответственно, нет иерархии типов. Каждый тип — сам по себе. Это называется «ортогональность».
В Go есть два способа изменять поведение во время выполнения: функции и интерфейсы.
Функции. На Go вполне можно писать в функциональном стиле. Замыкания есть. Анонимные функции есть. Функции можно передавать как параметры функциям. Функции можно возвращать из функций.
Вот те же хэндлеры и middleware в Echo:
type (
// ...
    // MiddlewareFunc defines a function to process middleware.
    MiddlewareFunc func(HandlerFunc) HandlerFunc

    // HandlerFunc defines a function to serve HTTP requests.
    HandlerFunc func(Context) error
// ...
Интерфейсы. Как и положено, интерфейс объявляет набор методов, доступных для объектов, реализующих этот интерфейс.
Вот, например, стандартный интерфейс в пакете fmt:
type Stringer interface {
    String() string
}
В Go нет ключевого слова implements. И вообще нет возможности явно сказать, что какой-то тип реализует интерфейс. Просто, если у типа есть все методы, объявленные в интерфейсе, он реализует интерфейс. И можно в переменную типа интерфейса, присвоить значение или указатель этого типа.
func (r Result) String() string {
    return fmt.Sprintf("success: %v", r.Success)
}

func println(s fmt.Stringer) {
    fmt.Println(s)
}
Интерфейсы и их реализации — ортогональны. Идея в том, чтобы делать интерфейсы максимально маленькими, и объявлять их в месте использования. Да, буковка «I» в SOLID. И не мучать множество реализаций необходимостью тщательно следить за тем, какие интерфейсы реализуются. Ну поменялся интерфейс, ну не будет этот тип теперь его реализовывать, ну и не надо. Всё будет проверено либо компилятором, либо выяснится при приведении типов в рантайме. Строгая утиная типизация.
Приведение типов работает интересно. Можно просто попробовать привести что угодно к нужному типу.
Это просто не скомпилируется, если компилятор может проверить типы.
something := Something{}
// invalid type assertion: something.(fmt.Stringer) (non-interface type Something on left)
s := something.(fmt.Stringer)
Это скомпилируется, если лишить компилятор сведений о типах. Но в рантайме случится паника (panic).
var something interface{}
something = Something{}
// panic: interface conversion: main.Something is not fmt.Stringer: missing method String
s := something.(fmt.Stringer)
Да, в Go универсальный тип для всего, включая также nil, — это interface{}. Его специально называют «empty interface».
А если слева от присваивания указать два значения, то паники не будет. Будет проверка, можно ли привести это значение к данному типу.
s, ok := something.(fmt.Stringer)
if !ok {
    fmt.Println("not Stringer")
}
Подобная же проверка работает и со встроенным типом map. Причем как для проверки существования ключа, так и для проверки, что значение по данному ключу приводится к нужному типу.
m := make(map[string]interface{})
m["one"] = 1
v, ok := m["one"]       // v: 1, ok: true
v, ok = m["two"]        // v: <nil>, ok: false
v, ok = m["one"].(string)   // v: , ok: false
Да, в Go функции могут возвращать более одного значения. Хотя эти проверки — вовсе не функции, а, скорее, конструкции языка. Так вот, функции могут возвращать более одного значения. И на этом построена концепция обработки ошибок в Go.
В Go считается, что ошибки — это лишь ошибки. Вполне нормальное дело, с которым нужно разобраться прямо сейчас. И незачем выкидывать какие-то исключения, чтобы обработать их фиг знает где там. Исключений в Go нет.
Если какая-то функция может завершиться с ошибкой, она возвращает два значения: результат выполнения и ошибку. Если всё хорошо, будет результат, а ошибка будет nil. Если всё плохо, результат будет nil или нулевое значение, а в ошибке будет написано, что не так.
Аналогичный подход использовался в JavaScript в эпоху колбэков. Только в Go результат и ошибка, это не параметры колбэка, а нормальные возвращаемые значения. Аналогичный подход используется в Rust, где функции возвращают монаду Result, в которой, соответственно, есть либо результат, либо ошибка.
Ошибкой в Go почти всегда является стандартный интерфейс error. Если функция не возвращает значения, но может завершиться с ошибкой, она просто возвращает error.
Вот пример из того же Echo. Тут эксплуатируются сразу несколько синтаксических особенностей Go.
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
    if c, err = ln.AcceptTCP(); err != nil {
        return
    } else if err = c.(*net.TCPConn).SetKeepAlive(true); err != nil {
        return
    } else if err = c.(*net.TCPConn).SetKeepAlivePeriod(3 * time.Minute); err != nil {
        return
    }
    return
}
Выходные значения могут иметь имя. Тогда можно в теле функции присвоить им значения, а потом просто сделать return. Почти как в Pascal.
if, как вы заметили, пишется без круглых скобок.
В условии if можно указать несколько выражений, через точку с запятой. Результатом проверки будет результат последнего выражения. Предыдущие выражения вовсе не обязаны возвращать bool. Поэтому часто выполнение функции, которая может завершиться с ошибкой, и проверку ошибки пишут одной строкой в операторе if. Хотя лично мне такая запись кажется плохочитабельной.
В Go неиспользуемая локальная переменная считается ошибкой компиляции. Поэтому, если уж вы получили этот err, то вам нужно будет что-то с ним сделать. Как минимум сравнить с nil.
Но если вы хотите проигнорировать значение ошибки, да и любое другое возвращаемое значение, можно использовать «переменную» _.
m := make(map[string]interface{})
v, _ := m["one"]

c, _ = ln.AcceptTCP()
Само собой, будьте осторожны. Если вы проигнорировали ошибку, то наверняка полученное значение может оказаться nil, и вы запросто словите "panic: runtime error: invalid memory address or nil pointer dereference". Да, NPE (в данном случае NPD) в Go имеется. И никакого контроля на nullability в языке не предусмотрено.
Бывает, что ошибки происходят совершенно неожиданно, и с ними ничего нельзя поделать прямо здесь и сейчас. В Go такая ситуация называется паникой. Обычно паника вызывает завершение текущего потока (точнее, горутины). Но панику можно обработать. Да, это аналогично механизму исключений, но описывается совсем другим синтаксисом.
Вот кусочек из пакета net/http. Оказывается, сервер находится там, а Echo — это лишь приятная обёртка, с роутингом и поддержкой JSON.
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
    // ...
    defer func() {
        if err := recover(); err != nil && err != ErrAbortHandler {
            const size = 64 << 10
            buf := make([]byte, size)
            buf = buf[:runtime.Stack(buf, false)]
            c.server.logf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
        }
        // ...
    }()
    // ...
    serverHandler{c.server}.ServeHTTP(w, w.req)
    // ...
}
Здесь у нас есть defer вызов анонимной функции. Этот вызов произойдёт при любом выходе из текущей функции, даже в случае паники. Это аналог finally блоков в других языках.
Здесь у нас используется встроенная функция recover(). Она отменяет панику и возвращает её причину, с которой можно что-то сделать.
Gopher
В Go почти не пишут точку с запятой. Тут, как в JavaScript, компилятор сам расставляет точки с запятой. Но делает это немного глючно. В результате, например, нельзя перенести открывающую круглую или фигурную скобку на новую строку, чтобы писать в стиле C. Ибо компилятор воткнёт в конец предыдущей строки точку с запятой, что сломает синтаксис. Поэтому спора о том, где открывать скобку, в Go нет.
В Go нет и других споров по поводу стиля написания кода. Потому что есть gofmt. Эта штуковина автоматически форматирует файлы исходников на Go. Как это предписано авторами языка. Достаточно запустить в своём пакете go fmt. А IDE сами автоматически форматируют, Idea делает это перед коммитом. И нет никаких споров.
Кстати, отступ в Go обозначается символом табуляции.
С одной стороны, Go — язык простой. Никакой перегрузки операторов. Никакой перегрузки методов и функций, диспетчеризация вызовов делается исключительно по имени функции. Динамическое связывание используется только для интерфейсов. Никакой поддержки иммутабельности. Всё ради эффективности компилятора. И компилятор действительно очень-очень быстрый.
И Go довольно близок к железу. Иногда из за этого наружу вылезают довольно странные несообразности. Например, структуры всегда передаются по значению, и если вы передаёте в функцию громадную структуру, она будет помещена в стек. Сами виноваты, используйте указатели. А вот срезы и мапы создаются в куче, и всегда передаются «по ссылке». Хотя эту «ссылку» вы в самом языке никогда не увидите. Но зато заметите, что вроде-как-передаваемая-по-значению мапа может изменяться сразу из нескольких мест кода.
С другой стороны, в Go притащили несколько мощных высокоуровневых концепций.
Тут есть сборщик мусора. В скомпилированный бинарник, ясен пень, включён рантайм Go. И там есть сборщик мусора.
В Go есть горутины (goroutine) и каналы (channel).
Горутины — это такой особый способ вызова функции так, что она начинает выполняться параллельно, в другом потоке. Это не значит, что на каждую горутину будет заведён отдельный поток. Это значит, что горутина будет запланирована на выполнение в пуле потоков. Если поток будет заблокирован, Go заведёт новый поток, и перебросит туда все горутины из этого потока, чтобы они могли выполняться. Go будет стараться, чтобы активных, не простаивающих, потоков было по числу CPU. По крайней мере, это справедливо в реализации Go от Google.
Это аналогично концепциям легковесных или «зелёных» потоков в некоторых библиотеках на других языках. Можно организовать массивное параллельное выполнение задач, но и не брезговать блокирующими операциями. Только в Go это является неотъемлемой частью языка и стандартной рантайм библиотеки.
В Go горутина не являетcя объектом. Это просто способ запуска функции (в том числе анонимной). Горутины нельзя явно подсчитать, нельзя явно остановить. Всё управление потоками спрятано от разработчика.
Горутины могут взаимодействовать через разделяемые данные. И тут потребуются примитивы синхронизации. И они есть в пакетах sync и sync/atomic.
Но интереснее взаимодействие через каналы. Это очень похоже на ZeroMQ сокеты. Некий, опциально буферизированный, поток структурированных данных (или сообщений). Одна горутина туда пишет структуры, и блокируется, если буфер заполнен. Другая горутина оттуда читает, и блокируется (опциально), если читать нечего. Можно наворотить красивые параллельные системы, почти акторы. И это всё — часть языка.
Вот из того же net/http сервера:
func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
    // ...
    done := make(chan struct{})
    // ...
    panicChan := make(chan interface{}, 1)
    go func() {
        defer func() {
            if p := recover(); p != nil {
                panicChan <- p
            }
        }()
        h.handler.ServeHTTP(tw, r)
        close(done)
    }()
    select {
    case p := <-panicChan:
        panic(p)
    case <-done:
        // ...
        w.WriteHeader(tw.code)
        w.Write(tw.wbuf.Bytes())
    // ...
    }
}
Ладно. Такого уровня Go-fu я ещё не достиг. Хотя Телеграм-бот на Go выглядит соблазнительно просто.
Что там наш веб сервер? Работает?
$ $GOPATH/bin/echo_sample

   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.0.0
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8080
Действительно работает.
$ curl "http://localhost:8080?name=Go"
{"message":"Hello, Go!"}
Один единственный бинарник размером 7.3 мегабайта, который будет работать почти в любом 64-битном Линуксе просто так, без установки всяких зависимостей, интерпретаторов или виртуальных машин. Это заметно меньше, чем аналогичный «Hello, world!» на Spring Boot. А при желании, можно собрать такой же и для Windows, и для FreeBSD. Не удивительно, что Go любят вся всяких мелких утилит, которые должны просто работать везде.
Go поддерживает много архитектур. И, так как весь бинарник собирается, по сути, исключительно из исходных текстов, особых проблем с кроссплатформенностью не должно быть.
Вселенная Go вообще выглядит довольно изолированной. Это всё — pure Go.

2019-01-27

О Максе Фрае

Макс Фрай. Max Frei. Без Макса. Максимально свободный.
Начинающий злобный угуландский колдун. Временно доперст. Вершитель. Демиург. Убийца. Опытный, но бестолковый сновидец. Герой-любовник. Воспитатель чудовищ и красавиц. Призванный демон. Испольняющий обязанности Сурта, и при этом друг Одина. Накх. Ключник. Существо исключительных судеб.
Дважды, трижды, четырежды(?) выдуманный персонаж, автор и читатель. Выдуманный собой и другими сочувствующими. Страстный любитель проживать свои (выдуманные и не только) и чужие судьбы.
Лабиринты, Хроники и Сновидения Ехо — лишь одна, наиболее подробно выдуманная и ярко описанная, судьба. Одна из.
Ехо
Да, я начитался Макса Фрая. В разных его ипостасях.
Понятно, что «Макс Фрай» — это лишь псевдоним. Но мне нравится думать, что сэр Макс написал правду. Что ему просто нужно было, чтобы множество Вершителей из этого нашего Мира Паука поверили и полюбили Ехо и весь Мир Стержня. Чтобы он продолжал существовать. И ясен пень, для издания книжек здесь нужно указать не только псевдоним, но и имя какого-то настоящего номинального автора.
Или какие-то странные выдумщики (а не только сэр Джуффин Халли) выдумали сэра Макса и его приключения. И издали вполне человеческим способом. А он не смог отказаться от искушения на самом деле прожить ещё и эту жизнь.
Не важно, на самом деле.
Хурон
Я понял, как называется эта скандинавская мумитролльская тоска. Это — тоска по несбывшемуся.
Всякое существо (и человеческое тут вовсе не исключение) хочет существовать, быть, сбываться. И сбываться максимально полно и разнообразно. Выдумки и сны этому только способствуют.
Для Макса наиболее привлекательны несбывшиеся чудеса. Он тоскует по чудесам. И делает (и не делает) всё возможное, чтобы чудес вокруг него случалось побольше.
Мы же тут стараемся не верить в чудеса. И несбывшееся для нас — это возможности, развилки судьбы (судьбокрёстки). Та самая вожжа, что под хвостом. То самое шило, что в заднице.
Мы мечемся, пробуем, путешествуем и перезжаем, учимся и учим, работаем и отдыхаем. Сбываемся всеми возможными способами. Лишь бы не скучать.
Или же читаем взапой о выдуманных мирах, смотрим выдуманные фильмы и сериалы, выдумываем свои и чужие сны. Сбегаем от «настоящей реальности» или творим другую. Пожалуй, это ничуть не хуже.
И гонит нас тоска по несбывшемуся. Страх не успеть.
Не стоит бояться. Не стоит печалиться. Это наша судьба. И, по-видимому, предназначение.

2019-01-06

О Keycloak

Есть такая штука. Называется Keycloak. Это не то, что вы подумали, а плащ или мантия, типа для ключей, или «ключевая мантия». Это сервер для Single-Sing-On (SSO), и для хранения учётных записей, и для всего такого, связанного с аутентификацией и авторизацией. Это — часть JBoss, который, оказывается, теперь называется WildFly, сервера приложений (Java EE) от RedHat. Кажется, это самый популярный среди свободных сервер SSO. Потому что на нас одновременно свалилось аж два с половиной проекта, где для авторизации юзеров используется именно Keycloak.
Keycloak old logo
Keycloak умеет OpenID Connect (aka OIDC) и SAML (aka Security Assertion Markup Language).
SAML — это привет из начала двухтысячных на считающимся ныне монструозным XML. Близкий друг действительно монструозного SOAP. Но теперь строить API на XML не модно. Поэтому забудем про SAML.
OpenID Connect будет поинтереснее. Он не имеет ничего общего с протоколами OpenID версий 1.1 и 2.0. Точнее, если верить Википедии, это следующая версия OpenID. И построено оно поверх OAuth 2.0.
OpenID Connect — это конкретный протокол аутентификации, построенный поверх фреймворка авторизации OAuth 2.0.
Помните, OAuth 2.0 — это лишь фреймворк. Там не прописано чётко, какими должны быть токены, как именно их получать, и прочие мелкие детали. Вот OpenID Connect эти детали конкретизирует. И у нас даже есть конкретная реализация: Keycloak.
OpenID Connect
Keycloak сервер — весь такой multitenancy. На нём положено создавать Realms — независимые пространства со своими юзерами, группами, ролями, ключами, страницами авторизации и клиентами. Клиентами (clients) в смысле OAuth, со своим ClientID и Redirect URI.
Если у нас есть Realm и мы знаем, где находится Keycloak, можно попробовать поавторизоваться. Есть у нас чётко определённый набор Endpointов, с которых можно начать. И их список тоже можно получить.
$ curl https://keycloak.example/auth/realms/gelin/.well-known/openid-configuration | jq .
{
  "issuer": "https://keycloak.example/auth/realms/gelin",
  "authorization_endpoint": "https://keycloak.example/auth/realms/gelin/protocol/openid-connect/auth",
  "token_endpoint": "https://keycloak.example/auth/realms/gelin/protocol/openid-connect/token",
  "token_introspection_endpoint": "https://keycloak.example/auth/realms/gelin/protocol/openid-connect/token/introspect",
  "userinfo_endpoint": "https://keycloak.example/auth/realms/gelin/protocol/openid-connect/userinfo",
  "end_session_endpoint": "https://keycloak.example/auth/realms/gelin/protocol/openid-connect/logout",
  "jwks_uri": "https://keycloak.example/auth/realms/gelin/protocol/openid-connect/certs",
...
Как положено в OAuth, попробуем получить токен.
$ curl -v \
  --url https://keycloak.example/auth/realms/gelin/protocol/openid-connect/auth \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'response_type=code&scope=openid&client_id=test&redirect_uri=http%3A%2F%2Flocalhost%3A8080'
В ответ нам выдадут форму логина, которая ставит кучку куки. В общем, логично.
Authorization code flow
Можно попробовать Implicit Flow.
$ curl -v \
  --url https://keycloak.example/auth/realms/gelin/protocol/openid-connect/auth \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'response_type=token&scope=openid&client_id=test&redirect_uri=http%3A%2F%2Flocalhost%3A8080&nonce=123'
В ответ будет подобная же форма логина.
Implicit flow
Ну типа почти обычный OAuth 2.0. Только нам точно известно, куда ходить за логином, где получать токен, где спрашивать сведения о пользователе.
Более того, токены у нас подписаны RSA ключом. И мы можем получить публичный ключ для их проверки.
$ curl https://keycloak.example/auth/realms/gelin/protocol/openid-connect/certs | jq .
{
  "keys": [
    {
      "kid": "AOaPQjC_jwnQG3tx4dqQYSKZyfgSlMLOjy_KFjikvcw",
      "kty": "RSA",
      "alg": "RS256",
      "use": "sig",
      "n": "j11PAJZ36mh0q1Inn1CsJZ7V_KckqZgFxzPgysrODl9P1k2-yNjpCWzY4UFJlqRalbYG9eLmb5gZAz_8R95BGj8C3u9vFOJYpWhj2PdHgb59KUPHvvn6CLJW_V1xac8uUcDlGnpxbFsztE19qlTFCeFOBhhdzHQW5tHtZhZZvdp6GVrCa9-CTKoP4LjRW7phJk-V-t93AkXWyXufXGzbpQzu4chcsfmgBDGaoDBm1_PomB_d6orOURdAIZi_-rkkcUz9Zz4e_Seo-RxeQ_p4LvWzmTFsPM4o3argnN7TYmT7G8iXeNw3pFQ0O-SUgz070-Ph0gl2mcWso7Pwya-MDQ",
      "e": "AQAB"
    }
  ]
}
В OpenID Connect, как положено, имеется Access Token, который нужно включать в заголовок Authorization, чтобы получить те же сведения о пользователе, и Refresh Token, который нужен для обновления Access Token. А ещё есть ID Token.
Именно ID Token является JWT токеном, который можно проверить публичным RSA ключом. И именно там содержатся некоторые сведения о пользователе. Можно не делать дополнительных запросов на Keycloak, а сразу извлечь всё, что надо, из ID Token.
На самом деле, все эти curlы вам не понадобятся. Ибо политикой Keycloak является использование адаптеров. То есть библиотек для ваших любимых языков программирования или middleware для ваших любимых веб фреймворков. Берёте нужный адаптер, настраиваете его JSON файлом, который можно скачать прямо в настройках клиента в админке Keycloak, и пользуетесь.
Если адаптера для Keycloak не нашлось, вполне может сгодиться адаптер для просто OpenID Connect. Их много, для разных языков (например, для Go).
Если у вас, допустим, есть какой-то UI на React, то вам достаточно подключить адаптер, и позаботиться о логине в нужный момент, и обновлении токенов, например, по таймеру.
import Keycloak from 'keycloak-js';

export const kc = Keycloak({
  url: process.env.REACT_APP_KEY_CLOAK_SERVER_URL,
  realm: process.env.REACT_APP_KEY_CLOAK_REALM,
  clientId: process.env.REACT_APP_KEY_CLOAK_CLIENT_ID,
});

const updateLocalStorage = () => {
  localStorage.setItem('kc_token', kc.token);
  localStorage.setItem('kc_refreshToken', kc.refreshToken);
};

kc.init({onLoad: 'login-required'})
  .success((authenticated) => {
    if (authenticated) {
      updateLocalStorage();

      setInterval(() => {
        kc.updateToken(11)
          .success((refreshed) => {
            if (refreshed) {
              updateLocalStorage();
            } else {
            }
          })
          .error(() => kc.logout());
      }, 10000);

      ReactDOM.render(app, document.getElementById('root'));
    } else {
      kc.login();
    }
  })
  .error(() => {
    kc.login();
  });

kc.onTokenExpired = () => {
  kc.logout();
  console.log('Token Expired');
};
Потом этот токен из Keycloak можно вставлять в заголовки запросов к API. А API уже будет проверять валидность этого токена.
Если взять адаптер для Spring Boot, то достаточно его подключить:
implementation 'org.keycloak:keycloak-spring-boot-2-starter:4.0.0.Final'
И настроить:
keycloak.realm: gelin
keycloak.resource: test
keycloak.auth-server-url: https://keycloak.example/auth
keycloak.public-client: true

keycloak.securityConstraints[0].authRoles[0]: user
keycloak.securityConstraints[0].authRoles[1]: admin
keycloak.securityConstraints[0].securityCollections[0].name: api resource
keycloak.securityConstraints[0].securityCollections[0].patterns[0]: /api
И всё. Эта магия работает.
Не нравится мне, когда такие довольно сложные для понимания штуки, как всякие токены, где, чтобы разобраться во всём, нужно нарисовать не одну диаграмму последовательностей, полностью закрываются очень простым фасадом. А если что-нибудь сломается, как выяснять, в чём проблема?

2018-12-08

О CORS

Современные браузеры не хотят просто так ходить на другие домены. Точнее на другой Origin. Это касается JavaScript, выполняемого в браузере.
Нельзя просто так взять, и потыкать API, находящееся в другом домене. И нельзя сделать crawler, работающий в браузере. Это будут cross-origin запросы. И они запрещены. Из соображений безопасности. Похоже, это скорее забота о том, чтобы конфиденциальные данные из вашего браузера не утекли куда не надо, чем попытка затруднить жизнь тем, кто захочет воспользоваться вашим API.
Но иногда у вас какой-нибудь SPA на React должен ходить в API на другой домен. И тут на помощь приходит CORS. Cross-Origin Resource Sharing. Сервер другого ресурса, нашего API, может явно сказать, с каких Origin к нему разрешён доступ. С помощью некоторых дополнительных HTTP заголовков.
В простейшем случае, когда наш JavaScript делает GET запрос, и не передаёт никаких потенциально опасных заголовков, браузер включает в запрос заголовок Origin. А сервер должен ответить с заголовком Access-Control-Allow-Origin, где, собственно, и говорит, разрешён ли доступ с этого Origin.
CORS Simple Request
В более сложном случае, например, когда мы делаем POST запрос с JSON в теле запроса, браузер делает так называемый "preflight request".
Это OPTIONS запрос, в который включены заголовки, описывающие будущий полноценный запрос. Access-Control-Request-Method говорит, какой HTTP метод будет в будущем запросе. Access-Control-Request-Headers говорит, какие HTTP заголовки будут в будущем запросе (помимо тех, вроде Host, User-Agent и Acept, которые и так разрешены по умолчанию). Конечно же, этот запрос включает заголовок Origin.
Если сервер согласен и с Origin, и с методом, и с заголовками, он отвечает 200 OK с дополнительными заголовками. Снова заголовок Access-Control-Allow-Origin выражает согласие сервера с Origin. Заголовок Access-Control-Allow-Methods перечисляет методы, которые сервер готов принимать с этого Origin. Заголовок Access-Control-Allow-Headers подтверждает готовность сервера принимать заголовки. Заголовок Access-Control-Max-Age указывает срок действия данного соглашения, браузер может какое-то время не повторять preflight запросы.
CORS Prefligh Request
В Access-Control-Allow-Origin сервер может вернуть не только тот самый Origin, что запросил доступ, но и специальное значение "*", звёздочку. Это означает "любой Origin". Но в этом случае запрещается передавать в запросе credentials. Под "credentials" подразумеваются куки, информация об аутентификации, включая заголовки и TLS сертификаты. Всё то, что браузер может неявно подмешать в запрос, и что явно идентифицирует пользователя.
По умолчанию credentials запрещены. Но сервер может разрешить, если выдаст заголовок Access-Control-Allow-Credentials со значением true. Но для Origin "*" это не работает.
Если говорить про fetch() в браузере, то для credentials есть свои опции.
fetch(url, {
    method: "POST", // *GET, POST, PUT, DELETE, etc.
    mode: "cors", // no-cors, cors, *same-origin
    cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
    credentials: "same-origin", // include, *same-origin, omit
    headers: {
        "Content-Type": "application/json; charset=utf-8",
        // "Content-Type": "application/x-www-form-urlencoded",
    },
    redirect: "follow", // manual, *follow, error
    referrer: "no-referrer", // no-referrer, *client
    body: JSON.stringify(data), // body data type must match "Content-Type" header
})
Сервер может ещё использовать заголовок Vary, чтобы указать на заголовки запроса, изменение которых приведёт к другому ответу сервера. Поэтому положено, если Access-Control-Allow-Origin возвращает конкретный Origin, то должен быть ещё и Vary: Origin.
Почти во всех случаях, когда у вас есть SPA и отдельное API, вам придётся учитывать CORS.
Вам повезло, если вы можете закрыть и фронтенд и бэкенд общим прокси на одном домене. Тогда ваше API будет, скажем, просто по относительному URL "/api", и это будет same-origin, и CORS не понадобится.
Вам повезло, если перед бэкендом вы можете поставить прокси, которое сможет добавлять заголовки. Например, Nginx. Тогда он сможет взять на себя всю работу c CORS. Не забудьте включить в Access-Control-Allow-Headers все заголовки, которые рожает ваше SPA.
location /api {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Origin,Accept,Key,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
    }
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Origin,Accept,Key,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
    proxy_pass http://127.0.0.1:8080/;
}
Если вам позарез нужны credentials, например, аутентификация у вас через cookie, то всё немного усложняется.
location /api {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '$http_origin';
        add_header 'Vary' 'Origin';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Origin,Accept,Key,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization';
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
    }
    add_header 'Access-Control-Allow-Origin' '$http_origin' always;
    add_header 'Vary' 'Origin';
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Origin,Accept,Key,Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
    proxy_pass http://127.0.0.1:8080/api;
}
Как правило, на preflight запрос сервер должен отвечать минуя все механизмы авторизации. Собственно, так и произойдёт, если вы к этому location добавите ещё, например, директивы auth_basic. Потому что return прерывает обработку запроса раньше.
Приведённые конфигурации открывают доступ к API с любого Origin. Возможно, это не то, что вы хотите. Тогда разбираться с CORS придётся в самом бэкенде.
Если вы разворачиваетесь в облаке, то вряд ли вам захочется запускать свой Nginx, лучше воспользоваться облачным load balancer. А они вовсе не обязаны втыкать дополнительные заголовки. К тому же в облаках статику и динамику как-то удобнее размещать в разных доменах, CDN, всё такое. Так что от CORS не отвертитесь, и снова на бэкенде.
В Spring Boot можно сделать так:
@Configuration
@EnableConfigurationProperties(CorsProperties::class)
open class CorsConfiguration {

    @Bean
    open fun corsConfigurer(
        corsProperties: CorsProperties?
    ): WebMvcConfigurer {
        return object : WebMvcConfigurer {
            override fun addCorsMappings(registry: CorsRegistry) {
                if (corsProperties != null && corsProperties.allowedOrigins.isNotEmpty()) {
                    registry.addMapping("/api/**").allowedOrigins(*corsProperties.allowedOrigins.toTypedArray())
                }
            }
        }
    }

}

@ConfigurationProperties(prefix = "web.cors")
open class CorsProperties {
    var allowedOrigins: List<String> = mutableListOf()
}
Тогда разрешить нужные Origin можно в application.yml:
web:
  cors:
    allowed-origins:
      - 'http://localhost:3000'
      - 'http://my.webapp.com'

2018-11-17

Об IPv6

Тихо и незаметно наступает эпоха IPv6. Уже можно заполучить IPv6 дома или в офисе, по крайней мере, клиентам ЭР-Телекома. Уже подключают к интернетам только по IPv6, по крайней мере, некоторых клиентов нашего заказчика, где-то в Америке. Уже без проблем можно получить IPv6 адрес для любого сервера любого уровня виртуальности почти у любого хостера или облачного провайдера. Уже встречаются дешманские виртуалки только с IPv6, и с IPv4 через NAT, где проброшен десяток портов.
ipv6
В IPv4, как помните, адрес 32-битный. Лишь четыре миллиарда адресов. И они закончились ещё в 2011.
В IPv6 адрес уже 128 бит. Этого хватит всем. Всем землянам, по крайней мере. И миллионам их карманных устройств, у каждого.
IPv6 адреса записываются в виде шестнадцатиричных чисел. Восемь четырёхзначных чисел, разделённых двоеточием. Начальные нули в каждом числе можно упустить. Самую длинную группу из нулевых чисел тоже можно упустить. В результате адрес localhost выглядит как ::1.
$ ip addr show dev lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
IPv6 адреса на интерфейсе, глядящем в Интернет, выглядят примерно так:
$ ip -6 addr show dev wlp2s0
2: wlp2s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 fd9e:ecc8:b68d::13f/128 scope global noprefixroute
       valid_lft forever preferred_lft forever
    inet6 2a02:ffff:ffff:12f0::13f/128 scope global dynamic noprefixroute
       valid_lft 85082sec preferred_lft 2282sec
    inet6 fd9e:ecc8:b68d:0:49ba:acb9:11b5:5adb/64 scope global temporary dynamic
       valid_lft 597201sec preferred_lft 78558sec
    inet6 fd9e:ecc8:b68d:0:fcd2:44e9:bb4d:d28b/64 scope global mngtmpaddr noprefixroute
       valid_lft forever preferred_lft forever
    inet6 2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb/64 scope global temporary dynamic
       valid_lft 85081sec preferred_lft 2281sec
    inet6 2a02:ffff:ffff:12f0:c354:cdb3:9794:b0a/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 85081sec preferred_lft 2281sec
    inet6 fe80::ff0b:c674:5528:a3c8/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
Да. IPv6 адресов всегда больше одного.
Адрес, начинающийся с fe80::, который scope link, присутствует всегда. Просто, если у вас включен IPv6. Даже если вы никуда не подключены. Этот адрес назначается автоматически, и уникален для данного интерфейса.
Можно считать это неким аналогом адресов 169.254.0.0/16 в IPv4. Только в IPv4 адрес 169.254 выдаётся только если не удалось получить адрес другим способом, например, по DHCP. И на это иногда уходит несколько секунд при загрузке ОС.
А в IPv6 адрес fe80:: назначается сразу, локально. Он конструируется из MAC адреса. И, в результате, в IPv6 хост может сразу общаться с другими IPv6 хостами, правда, в рамках только локальной сети.
Этого вполне достаточно, чтобы запросить публичный маршрутизируемый адрес у ближайшего маршрутизатора. Таким образом, адреса получаются автоматически без привлечения других протоколов. Это называется Router Advertisement.
Адреса раздаются префиксами. Подсетями чудовищного размера с маской /64. Прикиньте, да, 1.8e+19 адресов на абонента.
В данном случае у нас виднеется два префикса.
$ sipcalc fd9e:ecc8:b68d:0:49ba:acb9:11b5:5adb/64
-[ipv6 : fd9e:ecc8:b68d:0:49ba:acb9:11b5:5adb/64] - 0

[IPV6 INFO]
Expanded Address        - fd9e:ecc8:b68d:0000:49ba:acb9:11b5:5adb
Compressed address      - fd9e:ecc8:b68d:0:49ba:acb9:11b5:5adb
Subnet prefix (masked)  - fd9e:ecc8:b68d:0:0:0:0:0/64
Address ID (masked)     - 0:0:0:0:49ba:acb9:11b5:5adb/64
Prefix address          - ffff:ffff:ffff:ffff:0:0:0:0
Prefix length           - 64
Address type            - Unassigned
Network range           - fd9e:ecc8:b68d:0000:0000:0000:0000:0000 -
                          fd9e:ecc8:b68d:0000:ffff:ffff:ffff:ffff
То, что начинается на fd9e:ecc8: — это "Unique Local Unicast" (ULA) адреса. Это аналог 192.168.0.0/16, или 10.xxx.xxx.xxx или 172.16.xxx.xxx. Это немаршрутизируемые адреса, применимые только в локальной сети. В данном случае эти адреса нам зачем-то выдал маршрутизатор на OpenWRT.
$ sipcalc 2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb/64
-[ipv6 : 2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb/64] - 0

[IPV6 INFO]
Expanded Address        - 2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb
Compressed address      - 2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb
Subnet prefix (masked)  - 2a02:ffff:ffff:12f0:0:0:0:0/64
Address ID (masked)     - 0:0:0:0:49ba:acb9:11b5:5adb/64
Prefix address          - ffff:ffff:ffff:ffff:0:0:0:0
Prefix length           - 64
Address type            - Aggregatable Global Unicast Addresses
Network range           - 2a02:ffff:ffff:12f0:0000:0000:0000:0000 -
                          2a02:ffff:ffff:12f0:ffff:ffff:ffff:ffff
А вот префикс 2a02:ffff:ffff:12f0::/64 — это уже настоящий публичный префикс, выданный провайдером.
И в этом настоящем префиксе у нас почему-то аж три адреса.
$ ip -6 addr show dev wlp2s0 | grep 2a02
    inet6 2a02:ffff:ffff:12f0::13f/128 scope global dynamic noprefixroute
    inet6 2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb/64 scope global temporary dynamic
    inet6 2a02:ffff:ffff:12f0:c354:cdb3:9794:b0a/64 scope global dynamic mngtmpaddr noprefixroute
Адрес, помеченный mngtmpaddr, — это адрес, автоматически назначенный для данного префикса и данного MAC адреса. Точнее для EUI-64.
$ ipv6calc -i 2a02:ffff:ffff:12f0:c354:cdb3:9794:b0a/64
Address type: unicast, global-unicast, productive, iid, iid-global, iid-eui64
Interface identifier: c354:cdb3:9794:0b0a
EUI-64 identifier: c1:54:cd:b3:97:94:0b:0a
EUI-64 identifier is a global unique one
У этих адресов беда с безопасностью. Вторая половина IPv6 адреса у вашего компьютера будет всегда одной и той же, куда бы вы не перемещались, и какой бы IPv6 префикс вы не получали. Так можно отследить ваши перемещения.
Поэтому генерируется ещё второй адрес, помеченный как temporary. Здесь суффикс уже полностью случайный. Это называется "Privacy Extension" (RFC 3041).
$ ipv6calc -i 2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb/64
Address type: unicast, global-unicast, productive, iid-random, iid, iid-local
Interface identifier: 49ba:acb9:11b5:5adb
Interface identifier is probably generated by privacy extension
Последний адрес выдан DHCPv6. На самом деле, DHCP для IPv6 не особо нужен, и так адреса нормально назначаются. Но иногда, вроде, он нужен. По крайней мере у ЭР-Телекома нужно заполучать префиксы через DHCPv6 внутри PPPoE соединения.
$ ipv6calc -i 2a02:ffff:ffff:12f0::13f/128
Address type: unicast, global-unicast, productive, iid, iid-local
Interface identifier: 0000:0000:0000:013f
Interface identifier is probably manual set
Так под каким адресом мы ходим в интернет? В таблице маршрутизации всё запутанно.
$ ip -6 route show dev wlp2s0
2a02:ffff:ffff:12f0::13f proto kernel metric 600 pref medium
2a02:ffff:ffff:12f0::/64 proto ra metric 600 pref medium
fd9e:ecc8:b68d::13f proto kernel metric 600 pref medium
fd9e:ecc8:b68d::/64 proto ra metric 600 pref medium
fd9e:ecc8:b68d::/48 via fe80::c66e:1fff:feb9:e41b proto ra metric 600 pref medium
fe80::/64 proto kernel metric 256 pref medium
fe80::/64 proto kernel metric 600 pref medium
default via fe80::c66e:1fff:feb9:e41b proto ra metric 600 pref medium
Тут видно, что адрес машрутизатора (via) указан в виде локального адреса fe80::. Локальная сеть, оказывается, имеет большой префикс /48: fd9e:ecc8:b68d::/48. Адреса, полученные по DHCPv6, помечены как proto kernel. А адреса, полученные через Router Advertising, помечены как proto ra. И default маршрут тоже proto ra.
Выходит, что Router Advertising вроде как предпочтительнее DHCPv6. А temporary адрес, который случайный, должен быть предпочтительнее. И действительно, Яндекс Интернетометр говорит, что наш IPv6 адрес — 2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb. Собственно, остальные адреса и помечены как noprefixroute, то есть с ними не связаны маршруты.
Похоже, моя домашнаяя сеть — это какой-то IPv6 ад. На серваках всё проще.
# ip -6 addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qlen 1000
    inet6 2001:ffff:ffff:19c:5400:ff:fe54:a41/64 scope global mngtmpaddr dynamic
       valid_lft 2591971sec preferred_lft 604771sec
    inet6 fe80::5400:ff:fe54:a41/64 scope link
       valid_lft forever preferred_lft forever
Тут нет DHCPv6. Тут нет нужды в Privacy Extension, потому что серваки не ездят по планете. В результате у нас только два адреса: автоматический локальный fe80:: и настоящий публичный IPv6.
$ ip -6 route show dev eth0
2001:ffff:ffff:19c::/64  proto kernel  metric 256  expires 2591828sec
fe80::/64  proto kernel  metric 256
default via fe80::fc00:ff:fe54:a41  proto ra  metric 1024  expires 1628sec hoplimit 64
С маршрутами тоже всё просто и понятно.
Сетевые приложения как правило предпочитают IPv6 сокеты. Серверный IPv6 сокет способен принимать подключения как по IPv6, так и по IPv4, не пугайтесь.
# ss -lnpt
State   Recv-Q Send-Q  Local Address:Port  Peer Address:Port
LISTEN  0      128         127.0.0.1:6379             *:*     users:(("redis-server",pid=571,fd=4))
LISTEN  0      128                 *:80               *:*     users:(("nginx",pid=598,fd=6),("nginx",pid=596,fd=6))
LISTEN  0      128                 *:22               *:*     users:(("sshd",pid=555,fd=3))
LISTEN  0      128                 *:443              *:*     users:(("nginx",pid=598,fd=8),("nginx",pid=596,fd=8))
LISTEN  0      128                :::8080            :::*     users:(("java",pid=516,fd=39))
LISTEN  0      128                :::80              :::*     users:(("nginx",pid=598,fd=7),("nginx",pid=596,fd=7))
LISTEN  0      128                :::22              :::*     users:(("sshd",pid=555,fd=4))
LISTEN  0      128                :::443             :::*     users:(("nginx",pid=598,fd=9),("nginx",pid=596,fd=9))
А вот для Nginx нужно явно указать слушание обоих протоколов.
server {
    listen 80 default_server;
    listen 443 ssl default_server;
    listen [::]:80 default_server;
    listen [::]:443 ssl default_server;

    #...
В URL IPv6 адрес положено брать в квадратные скобки: http://[2a00:1450:4011:808::1001].
Для IPv6 адресов в DNS предусмотрен специальный тип записи AAAA.
$ dig +noall +question +answer aaaa google.com
;google.com.                    IN      AAAA
google.com.             299     IN      AAAA    2a00:1450:4011:808::1008
У самих DNS серверов, включая публичные DNS гугла, тоже есть IPv6 адреса.
$ dig +noall +question +answer +stats @2001:4860:4860::8888 aaaa google.com
;google.com.                    IN      AAAA
google.com.             271     IN      AAAA    2a00:1450:4011:80b::1002
;; Query time: 48 msec
;; SERVER: 2001:4860:4860::8888#53(2001:4860:4860::8888)
;; WHEN: Sat Nov 17 19:38:19 +06 2018
;; MSG SIZE  rcvd: 67
$ dig +noall +question +answer +stats @2001:4860:4860::8844 aaaa google.com
;google.com.                    IN      AAAA
google.com.             299     IN      AAAA    2a00:1450:4011:80e::1009
;; Query time: 52 msec
;; SERVER: 2001:4860:4860::8844#53(2001:4860:4860::8844)
;; WHEN: Sat Nov 17 19:39:00 +06 2018
;; MSG SIZE  rcvd: 67
$ dig +noall +question +answer +stats @2606:4700:4700::1111 aaaa google.com
;google.com.                    IN      AAAA
google.com.             188     IN      AAAA    2a00:1450:4011:804::1004
;; Query time: 35 msec
;; SERVER: 2606:4700:4700::1111#53(2606:4700:4700::1111)
;; WHEN: Sat Nov 17 19:40:06 +06 2018
;; MSG SIZE  rcvd: 67
$ dig +noall +question +answer +stats @2606:4700:4700::1001 aaaa google.com
;google.com.                    IN      AAAA
google.com.             157     IN      AAAA    2a00:1450:4011:804::1004
;; Query time: 35 msec
;; SERVER: 2606:4700:4700::1001#53(2606:4700:4700::1001)
;; WHEN: Sat Nov 17 19:40:37 +06 2018
;; MSG SIZE  rcvd: 67
IPv6 вполне поддерживается в соответствующих типах данных PostgreSQL.
postgres=# select network('2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb/64');
         network
--------------------------
 2a02:ffff:ffff:12f0::/64
(1 строка)

postgres=# select inet '2a02:ffff:ffff:12f0:49ba:acb9:11b5:5adb/64' && cidr '2a02:ffff:ffff:12f0::/64';
 ?column?
----------
 t
(1 строка)
Вроде как существует возможность, имея только IPv6 адрес, ходить в IPv4 сети. Чтобы занатить IPv6 адреса в IPv4, есть NAT64. Чтобы резолвить то, что резолвится только в IPv4, в IPv6 адреса, есть DNS64.
Доля мирового IPv6 трафика уже доходит до 25%. Так что пора, пора приобщаться к новому Интернету. Пока не поздно.
ipv6 adoption
К сожалению, ЭР-Телеком не выдаёт статические IPv6 префиксы. То есть IPv6 вы попробовать сможете, но вот поднять у себя сервер без DynDNS не выйдет. Ну тоже неплохо.
И не забывайте, что IPv6 — это настоящий адрес. Безо всякого NAT. Так что настраивайте файерволы на маршрутизаторах, чтобы ваши телефоны в вайфае не похакали. В OpenWRT по дефолту всё норм, входящие соединения запрещены, получается как за NAT.