Pull to refresh

Прокси-сервер для Android на Go

Level of difficultyMedium
Reading time14 min
Views7.8K

Реализация простого HTTP CONNECT прокси-сервера на Go, квест с маркировкой сетевых пакетов и запуск программы в Android.

Интро

После долгих лет работы разработчиком софта я хочу быть... всё тем же разработчиком. Мне это так же интересно, как и четверть века назад, когда я только начинал программировать. Даже больше - за это время количество того, чего я не знаю, многократно увеличилось, и всё стало ещё интереснее. Это та стихия, в которой я чувствую себя как рыба в воде.

Практически каждый проект с моим участием связан с какой-то историей, настолько необычной и запоминающейся, что каждый раз я ощущаю себя как в путешествии в фильме Трасса 60, с остановками, успехами и фейлами, не всегда с ожидаемым результатом, но..

"Какая разница, что в коробке. Истории, которыми она обросла, теперь намного важнее."

Большинство проектов ушло в корзину, от них остался только опыт. Некоторыми я пользуюсь время от времени. Но результатом моего недавнего творения я пользуюсь каждый день.

Идея

В какой-то момент я понял, что мне не хватает контроля над трафиком, проходящим через мой телефон. Всякое бывает. Интернет в роуминге стоит денег. Интернет-пакеты с туристических симок заканчиваются довольно быстро. Что происходит внутри китайских смартфонов - это вообще шапито. Я не потребляю столько интернет-трафика, сколько фактически тратится.

При этом на телефоне всё лишнее убрано через adb. Все обновления в ручном режиме. Выбранный в браузере DNS-сервер отсекает часть трекеров и рекламных площадок. Блокировщик рекламы режет её по мере возможности.

Но современный софт и сайты способны жрать всё больше и больше. Да ещё и сотовым операторам иногда взбредает в голову ввести плату за расшаривание интернета.

Поэтому я захотел сделать вот такую штуку:

Прокси на телефоне, через которую я смогу раздавать интернет без включения мобильной точки доступа. В которой можно фильтровать и анализировать потребление трафика.

Но не взять что-то готовое, а начать новую увлекательную историю. Сделать приложение на Golang. Без Android Studio, Kotlin и Gradle. По возможности без фреймворков или сторонних библиотек.

Спойлер

Забегая вперёд, скажу, что у меня всё получилось. Приложение работает почти на всех моих устройствах (кроме зубной щётки и термометра). Проксирует, фильтрует, управляется локально и удалённо.

Например, вот так выглядела исходная статистика проксирования при заходе в браузере на главную страницу mail.ru:

Скриншот

С 43 доменов тянулось около 7 мегабайт всякого мусора.

И вот такой она стала после фильтрации трафика:

Скриншот

Около одного мегабайта с 6 доменов. Экономия трафика в 7 раз.

HTTP прокси на Go

Самый распространённый тип прокси (и доступный для настройки в Android) - обычный HTTP-прокси, поддерживающий HTTP метод CONNECT и создающий туннель между клиентом и целевым сервером. Сделать CONNECT-прокси сервер в Go довольно просто. В стандартной библиотеке Go нет явной реализации этого типа прокси, но зато она встречается в юнит-тестах в исходниках net-пакетов Go, например в коде "go/src/net/http/transport_test.go"

Суть CONNECT-прокси приятна в своей простоте. Клиент делает HTTP CONNECT запрос к прокси с указанием сервера, с которым нужно установить соединение. Например:

CONNECT google.com:443 HTTP/1.1

Прокси в свою очередь подключается к указанному серверу и затем занимается перекачиванием данных от клиента к серверу и наоборот. Всё остальное, что происходит в получившемся туннеле (шифрование, сжатие данных) - дело клиента и целевого сервера.

Если всё сильно упростить, то код прокси-сервера может выглядеть так:

package main

import (
  "crypto/tls"
  "fmt"
  "io"
  "log"
  "net"
  "net/http"
)

const proxyListenAddress = "0.0.0.0:3128"

func main() {
  proxyServer := http.Server{
    Addr:         proxyListenAddress,
    Handler:      http.HandlerFunc(connectHandler),
    TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
  }
  log.Println("Started proxy at:", proxyServer.Addr)
  if err := proxyServer.ListenAndServe(); err != nil {
    log.Println("Server failed:", err)
  }
}

func connectHandler(w http.ResponseWriter, r *http.Request) {
  if r.Method != http.MethodConnect {
    log.Println("Method not allowed:", r.Method)
    w.WriteHeader(http.StatusMethodNotAllowed)
    return
  }

  log.Println("Hijacking connection:", r.RemoteAddr, "->", r.URL.Host)
  clientConn, _, err := w.(http.Hijacker).Hijack()
  if err != nil {
    log.Println("Hijack error:", err)
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
  }
  defer clientConn.Close()

  log.Println("Connecting to:", r.URL.Host)
  targetConn, err := net.Dial("tcp", r.URL.Host)
  if err != nil {
    log.Println("Connect error:", err)
    writeRawResponse(clientConn, http.StatusServiceUnavailable, r)
    return
  }
  defer targetConn.Close()

  writeRawResponse(clientConn, http.StatusOK, r)

  log.Println("Transferring:", r.RemoteAddr, "->", r.URL.Host)
  go func() {
    io.Copy(targetConn, clientConn)
    targetConn.Close()
  }()
  io.Copy(clientConn, targetConn)
  log.Println("Done:", r.RemoteAddr, "->", r.URL.Host)
}

func writeRawResponse(conn net.Conn, statusCode int, r *http.Request) {
  if _, err := fmt.Fprintf(conn, "HTTP/%d.%d %03d %s\r\n\r\n", r.ProtoMajor,
    r.ProtoMinor, statusCode, http.StatusText(statusCode)); err != nil {
    log.Println("Writing response failed:", err)
  }
}

Собираю:

go build -o bin/simple-proxy

Запускаю на ноутбуке с линуксом, проверяю курлом и браузером:

curl --proxytunnel -v --proxy http://127.0.0.1:3128 https://google.com
chrome --proxy-server=http://127.0.0.1:3128

Ура, отлично работает! Затем кросс-компиляция, запуск в винде, затем на ARM-одноплатнике - тоже всё ок.

Что же андроид? Собираю для андроида:

GOOS=android GOARCH=arm64 go build -o bin/simple-proxy

Включаю на телефоне одновременно Wi-Fi и передачу мобильных данных (как на диаграмме в начале текста). Копирую и запускаю исполняемый файл через adb:

adb push bin/simple-proxy /data/local/tmp
adb shell "cd /data/local/tmp && chmod u+x simple-proxy && ./simple-proxy"

Запускается, но как прокси это, увы, не работает:

Started proxy at: 0.0.0.0:3128
Hijacking connection: 127.0.0.1:49467 -> google.com:443
Connecting to: google.com:443
Connect error: dial tcp: lookup google.com on [::1]:53: read udp [::1]:54776->[::1]:53: read: connection refused

При запуске на андроиде в termux результат такой же.

Go resolver

Ошибка из предыдущего лога говорит о том, что, приложению не удаётся достучаться до локального DNS и получить ip-адрес для хоста "google.com".

Специфика работы DNS-ресолвера в Go поверхностно описана в официальной документации Name Resolution, детали же можно посмотреть в исходниках Go (в "go/src/net").

Итак, в винде это сработало, потому что приложение под капотом вызывает WinAPI функции типа GetAddrInfoW. В линкусе оно либо использует DNS сервер и настройки, указанные в /etc/resolv.conf; либо обращается к локальному DNS сервису типа systemd-resolved, который знает, как и куда сходить для ресолва адресов.

В андроиде же всё неоднозначно, согласно AOSPдокументации DNS Resolver

Ок, кастомизировать поведение ресолвера в Go не очень сложно. Пусть пока он напрямую ходит в DNS. К тому же теперь появляется возможность выбора DNS-сервера (например, Adguard DNS, который отрежет часть трекеров и рекламных площадок). Да и в принципе, в своём ресолвере можно прикрутить нормальные протоколы типа DoH/DoT (или кто знает, что ещё понадобится в этом мире завтра).

В предыдущий код добавляю простейшие диалер с ресолвером:

const dnsAddress = "8.8.8.8:53" //Google DNS
var dialer = createDialer()

func createDialer() *net.Dialer {
	resolverDialer := &net.Dialer{}
	resolverDial := func(ctx context.Context, network, addr string) (net.Conn, error) {
		log.Println("Resolver dial:", network, addr)
		return resolverDialer.DialContext(ctx, network, dnsAddress)
	}

	resolver := &net.Resolver{PreferGo: true, Dial: resolverDial}
	return &net.Dialer{Resolver: resolver, Timeout: 60 * time.Second}
}

И использую его для создания соединения:

targetConn, err := dialer.Dial("tcp", r.URL.Host)

Не самая идеальная реализация, но для проверки подойдёт. Собираю для андроида, запускаю на телефоне:

GOOS=android GOARCH=arm64 go build -o bin/simple-proxy-resolve
adb push bin/simple-proxy-resolve /data/local/tmp
adb shell "cd /data/local/tmp && chmod u+x simple-proxy-resolve && ./simple-proxy-resolve"

Проверяю, и ошибка меняется на:

2024/02/10 15:48:13 Connect error: dial tcp: lookup google.com on [::1]:53: read udp 192.168.1.61:52554->8.8.8.8:53: i/o timeout

Приложение теперь не может достучаться до указанного DNS. Оно пытается лезть в интернет через вайфай (который не подключён к интернету) и игнорирует наличие мобильной сети.

Действительно, стоит отключить вайфай на телефоне, и всё работает:

Transferring: 127.0.0.1:49682 -> google.com:443
Done: 127.0.0.1:49682 -> google.com:443

В андроиде при включённом вайфае весь трафик пытается идти только через вайфай. Известная проблема, не имеющая единого решения на всём многообразии версий Android и производителей смартфонов. Часто доходит до смешного - человек подключается с телефона к стиральной машинке, и на это время теряет доступ в интернет.

Маркировка сетевых пакетов в Android

По сути, задача сводится к классической: есть несколько сетевых интерфейсов и часть трафика надо гнать через определённый интерфейс. В разных системах это делается по-разному. Часто решается настройкой маршрутизации. Но трогать маршрутизацию на нерутованном андроиде та ещё задача.

Быстрый поиск решения в интернете приводит к подобному Java-коду:

import android.app.Activity;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkRequest;
import android.os.Bundle;
import android.util.Log;

import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ConnectivityManager connectivityManager =
            (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);

        connectivityManager.registerNetworkCallback(
            new NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(),
            new ConnectivityManager.NetworkCallback() {
                @Override
                public void onAvailable(Network network) {
                    connectivityManager.bindProcessToNetwork(network);
                    Log.i("GoLog", "Cellular network: " + network.toString());
                }
            }
        );
    }
}

То-есть у обычного APK приложения на Java есть возможность привязаться к определённой сети. Но, если что-то может сделать Java-приложение, то наверняка это же может сделать и нативное приложение.

Исходники андроида открыты. Пройдя по такой цепочке в исходном коде из репозиториев https://android.googlesource.com/platform/packages/modules/Connectivity и https://android.googlesource.com/platform/system/netd:

packages/modules/Connectivity/framework/src/android/net/
  - ConnectivityManager.java:bindProcessToNetwork
  - ConnectivityManager.java:setProcessDefaultNetwork
  - NetworkUtils.java:bindProcessToNetwork
  - NetworkUtils.java:bindProcessToNetworkHandle
packages/modules/Connectivity/framework/jni/
  - android_net_NetworkUtils.cpp:android_net_utils_bindProcessToNetworkHandle
frameworks/base/native/android/
  - net.c:android_setprocnetwork
system/netd/client/
  - NetdClient.cpp:setNetworkForProcess
  - NetdClient.cpp:setNetworkForTarget
  - NetdClient.cpp:setNetworkForSocket
  - FwmarkClient.cpp:send
system/netd/server/
  - FwmarkServer.cpp:processClient

можно прийти к строке кода:

setsockopt(*socketFd, SOL_SOCKET, SO_MARK, &fwmark.intValue, sizeof(fwmark.intValue))

где fwmark.intValue - это 32 бита, в которых содержится информация для маршрутизации передаваемых через этот сокет пакетов в указанную сеть. Вот так в C-коде разложены эти биты:

union Fwmark {
    uint32_t intValue;
    struct {
        uint16_t netId             : 16;
        uint8_t explicitlySelected :  1;
        uint8_t protectedFromVpn   :  1;
        uint8_t permission         :  2;
        uint8_t uidBillingDone     :  1;
        uint8_t reserved           :  8;
        uint8_t vendor             :  2;
        uint8_t ingress_cpu_wakeup :  1;
    };

    constexpr Fwmark() : intValue(0) {}
};

В "system/netd/server/FwmarkServer.cpp" можно посмотреть как правильно заполнить этот юнион.

Но вот этот netId (число, начинающееся с 100 и инкрементирующееся при каждом подключении к сети) узнать в не-Java приложении может оказаться довольно нетривиальной задачей. Ок, на самом деле на этом этапе можно собрать APK приложение из тех 10 строк Java-кода из начала главы, чтобы распечатать айдишник сети в лог.

Без особой надежды, но первое, что я решил попробовать, узнав netId - это вызвать в Go-коде:

syscall.SetsockoptInt(socketFd, SOL_SOCKET, SO_MARK, fwmarkIntValue)

на что получил от андроида ожидаемое "operation not permitted". Приложение в системных вызовах довольно ограничено.

Но исходники "system/netd" дают ответ, как это сделать из непривилегированного кода.

Сборка Go+C

Итак, одно из решений предыдущей проблемы - привязать те Go-сокеты, которые должны общаться с внешним миром, к интерфейсу мобильной передачи данных, используя "netd/client" как прослойку.

Код в "system/netd/client/NetdClient.cpp" предоставляет доступные функции для привязки сокета к сети:

extern "C" int setNetworkForSocket(unsigned netId, int socketFd) {
    if (socketFd < 0) {
        return -EBADF;
    }
    FwmarkCommand command = {FwmarkCommand::SELECT_NETWORK, netId, 0};
    return FwmarkClient().send(&command, socketFd);
}

То-есть, если вызвать "NetdClient:setNetworkForSocket" после создания сокета в Go, но до момента отправки данных - в этом случае передаваемые в сокет данные уйдут к указанную сеть.

Добавляю немного кросс-языкового кода:

//go:build android && cgo

package main

/*
#include <NetdClient.h>
*/
import "C"

func setNetworkForSocket(socket uintptr, networkId uint) {
  C.setNetworkForSocket(C.uint(networkId), C.int(socket))
}

и вызываю этот метод в Go после создания сокета:

const networkId = 269 // current netId of the cellular network on my phone

func createDialer() *net.Dialer {
	dialerControl := func(network, address string, conn syscall.RawConn) error {
		controlFunc := func(socket uintptr) {
			log.Println("Dialer control:", socket, network, address)
			setNetworkForSocket(socket, networkId)
		}
		return conn.Control(controlFunc)
	}

	resolverDialer := &net.Dialer{Control: dialerControl}
	resolverDial := func(ctx context.Context, network, addr string) (net.Conn, error) {
		log.Println("Resolver dial:", network, addr)
		return resolverDialer.DialContext(ctx, network, dnsAddress)
	}

	resolver := &net.Resolver{PreferGo: true, Dial: resolverDial}
	return &net.Dialer{
		Control:  dialerControl,
		Resolver: resolver,
		Timeout:  60 * time.Second,
	}
}
Полная версия получившегося Go кода
//go:build android && cgo

package main

import (
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"syscall"
	"time"
)

/*
#include <NetdClient.h>
*/
import "C"

func setNetworkForSocket(socket uintptr, networkId uint) {
	C.setNetworkForSocket(C.uint(networkId), C.int(socket))
}

const proxyListenAddress = "0.0.0.0:3128"

func main() {
	proxyServer := http.Server{
		Addr:         proxyListenAddress,
		Handler:      http.HandlerFunc(connectHandler),
		TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
	}
	log.Println("Started proxy at:", proxyServer.Addr)
	if err := proxyServer.ListenAndServe(); err != nil {
		log.Println("Server failed:", err)
	}
}

func connectHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodConnect {
		log.Println("Method not allowed:", r.Method)
		w.WriteHeader(http.StatusMethodNotAllowed)
		return
	}

	log.Println("Hijacking connection:", r.RemoteAddr, "->", r.URL.Host)
	clientConn, _, err := w.(http.Hijacker).Hijack()
	if err != nil {
		log.Println("Hijack error:", err)
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	defer clientConn.Close()

	log.Println("Connecting to:", r.URL.Host)
	targetConn, err := dialer.Dial("tcp", r.URL.Host)
	if err != nil {
		log.Println("Connect error:", err)
		writeRawResponse(clientConn, http.StatusServiceUnavailable, r)
		return
	}
	defer targetConn.Close()

	writeRawResponse(clientConn, http.StatusOK, r)

	log.Println("Transferring:", r.RemoteAddr, "->", r.URL.Host)
	go func() {
		io.Copy(targetConn, clientConn)
		targetConn.Close()
	}()
	io.Copy(clientConn, targetConn)
	log.Println("Done:", r.RemoteAddr, "->", r.URL.Host)
}

func writeRawResponse(conn net.Conn, statusCode int, r *http.Request) {
	if _, err := fmt.Fprintf(conn, "HTTP/%d.%d %03d %s\r\n\r\n", r.ProtoMajor,
		r.ProtoMinor, statusCode, http.StatusText(statusCode)); err != nil {
		log.Println("Writing response failed:", err)
	}
}

const dnsAddress = "8.8.8.8:53" //Google DNS
var dialer = createDialer()

const networkId = 269

func createDialer() *net.Dialer {
	dialerControl := func(network, address string, conn syscall.RawConn) error {
		controlFunc := func(socket uintptr) {
			log.Println("Dialer control:", socket, network, address)
			setNetworkForSocket(socket, networkId)
		}
		return conn.Control(controlFunc)
	}

	resolverDialer := &net.Dialer{Control: dialerControl}
	resolverDial := func(ctx context.Context, network, addr string) (net.Conn, error) {
		log.Println("Resolver dial:", network, addr)
		return resolverDialer.DialContext(ctx, network, dnsAddress)
	}

	resolver := &net.Resolver{PreferGo: true, Dial: resolverDial}
	return &net.Dialer{
		Control:  dialerControl,
		Resolver: resolver,
		Timeout:  60 * time.Second,
	}
}

Теперь нужно собрать Go-бинарник не совсем традиционным способом - используя тулчейн из Android NDK. Его можно скачать напрямую с официального сайта: "https://dl.google.com/android/repository/android-ndk-r26b-linux.zip"

Распаковываю его в "~/android_sdk".

Можно установить то же самое, следуя Android-way, используя команду `sdkmanager "ndk;26.2.11394342"` из Android SDK Command-line Tools.

Ещё мне понадобятся исходники netd. Смартфон, на котором я планирую проверять приложение на Android 7.1, поэтому качаю исходники для моей версии:

git clone --depth 1 --branch android-7.1.2_r39 --single-branch https://android.googlesource.com/platform/system/netd

Складываю их в "~/android_sdk/sources-misc/android-7.1.2_r39".

Вытаскиваю из телефона либу с реализацией netd-клиента и её зависимость, чтобы слинковать их с моим исполняемым файлом:

mkdir ./lib64
adb pull /system/lib64/libnetd_client.so ./lib64/
adb pull /system/lib64/libc++.so ./lib64/

На этот раз сборка чуть сложнее:

#!/usr/bin/env bash

TOOLCHAIN=~/android_sdk/ndk/26.2.11394342/toolchains/llvm/prebuilt/linux-x86_64
ANDROID_SRC=~/android_sdk/sources-misc/android-7.1.2_r39
ANDROID_API=25

export GOOS=android
export GOARCH=arm64
export CGO_ENABLED=1
export CGO_LDFLAGS="-Llib64 -lnetd_client -lc++"
export CGO_CFLAGS="-I$ANDROID_SRC/platform/system/netd/include"
export CC=$TOOLCHAIN/bin/aarch64-linux-android$ANDROID_API-clang
export CXX=$TOOLCHAIN/bin/aarch64-linux-android$ANDROID_API-clang++

go build -trimpath -o bin/simple-proxy-bind

Запускаю:

adb push bin/simple-proxy-bind /data/local/tmp
adb shell "cd /data/local/tmp && chmod u+x simple-proxy-bind && ./simple-proxy-bind"

Проверяю. Работает!

Хоть всё и собрано на коленке, захардкожен netId, статический бинарник собран под конкретную платформу, запускается не самым удобным образом, но это прототип, который делает то, что нужно. И это был, наверное, самый интересный технический момент в проекте.

Дорисовка совы

Дальнейшее описание может напоминать второй шаг из мема "Как нарисовать сову". Но там довольно рутинная работа без особых технических сложностей.

Кратко, что я сделал, чтобы пользоваться приложением в повседневной жизни:

  • Пересобрал его в виде APK при помощи gomobile, чтобы запускать как обычное андроид-приложение (хотя позже я отказался от `gomobile build` в пользу самостоятельного управления сборкой).

  • Добавил кеширование DNS-ответов и фильтрацию хостов по спискам whitelist/blacklist.

  • Сделал на Java минимальную реализацию Android Activity для автоматизации проброса netId из Java-рантайма в Go и заменил ею GoNativeActivity, которую создаёт gomobile. В этой же активити я отрисовываю пару метрик и кнопку перехода в полноценный веб-интерфейс.

  • Сделал интерфейс для управления приложением и просмотра статистики (на встроенном в приложение веб-сервере).

  • Перепаковал и переподписал APK с этими добавками.

Инструменты, которые я использовал при разработке:

  • GNU make - для сборки всего

  • go - для компиляции go-кода

  • clang из Android NDK - для кросс-компиляции с использованием С-кода Android

  • javac из OpenJDK 17 - для компиляции Activity на Java

  • zip и aapt2, apksigner, d8 из Android Platform Tools - для сборки APK

  • adb - для консольного доступа к телефону

  • sass и swc - для сборки юая

Разные сценарии сборки позволяют собрать не только Android-приложение, но и обычные консольные программы для Windows или Linux.

UI

С самого начала я решил, что у приложения будет универсальный веб-интерфейс для локального и удалённого управления. Простой и лёгкий. Без фреймворков и сторонних библиотек. Без NodeJS, npm и всех этих бесконечных node_modules.

Но после программирования на TypeScript не особо хочется возвращаться на голый JS. И появление SWC стало тем глотком свежего воздуха, который позволил мне использовать TS, и при этом отказаться от NodeJS (по крайней мере в этом проекте).

В результате юай стал таким легковесным, каким (в моём понимании) он и должен быть.

Использование

Я часто пользуюсь прокси, чтобы заблокировать мусорный трафик от браузера и приложений. Наиболее используемый сценарий немного отличается от того, что был показан на изначальной диаграмме. В этом сценарии я:

  • Запускаю прокси на телефоне.

  • Подключаю этот телефон к вайфай сети, в которой нет доступа в интернет.

  • В настройках вайфай-соединения на телефоне указываю в качестве прокси себя же (127.0.0.1)

  • После этого трафик от приложений идёт через прокси, фильтруется и передаётся в мобильную сеть.

  • В статистике становится видно хосты, с которыми связываются приложения, и объём передаваемых данных. Эту информацию можно использовать для пополнения списков блокировки.

В разных версиях Android и у разных производителей свои особенности поведения системы и настройки, которые приходится учитывать.

Скорее всего, есть готовые подобные решения. Но, как я уже говорил - мне просто очень нравится программировать.

Я планирую опубликовать исходники, и, может быть, написать про некоторые другие интересные моменты. История ещё не закончена.

Go?

Вероятно, кто-то скажет: "-А зачем здесь Go? На Java это сделать правильнее и проще, для Android уж точно!"

Да, всё верно. Если делать для Android. Но хочется запускать программу на чём угодно (в разумных пределах), и Go для этого вполне хорош.

На телефоне мне очень не хватает полноценной штатной консоли и `su` или `sudo`. Без них чувство, что тебя на время пустили поиграть в чужую песочницу. Держишь в руках это удобное высокопроизводительное комбо: процессор, память, сетевые интерфейсы; автономное, с дисплеем, да ещё в таком компактном формате, о котором когда-то можно было только мечтать. И программировать для него хочется так же просто, как для привычных систем. На C, Go или старом добром Паскале.

Поэтому, почему бы и не Go.

Хотя да, в результате версия прокси для андроида получилась таким Франкенштейном на смеси Go, Java и C.

Хабру

Я много лет читаю Хабр. Для меня в последнее время он стал тем центром, который постоянно подогревает интерес и не даёт потерять себя в профессиональном плане (что особенно чувствуется в эпоху удалёнки).

Это уникальное место, где можно виртуально пройтись по испанской барахолке, найти решение давней проблемы с радио-модулем, заглянуть в космос, прочитать про детали древнего протокола, узнать как подготавливать древесину для столешницы, вспомнить про старые девайсы и увидеть новые. Всё разнообразно, многогранно, и неизменно интересно.

Хабр, оставайся собой! Хабом знаний, которые будут актуальны даже спустя годы. Не становись блогом с засильем рекламы в статьях-однодневках.

Tags:
Hubs:
Total votes 35: ↑35 and ↓0+35
Comments16

Articles