Profile img

Hi, I'm who's behind Fedify, Hollo, BotKit, and this website, Hackers' Pub! My main account is at @hongminhee洪 民憙 (Hong Minhee) :nonbinary:.

Fedify, Hollo, BotKit, 그리고 보고 계신 이 사이트 Hackers' Pub을 만들고 있습니다. 제 메인 계정은: @hongminhee洪 民憙 (Hong Minhee) :nonbinary:.

FedifyHolloBotKit、そしてこのサイト、Hackers' Pubを作っています。私のメインアカウントは「@hongminhee洪 民憙 (Hong Minhee) :nonbinary:」に。

Website
hongminhee.org
GitHub
@dahlia
Hollo
@hongminhee@hollo.social
DEV
@hongminhee
velog
@hongminhee
Qiita
@hongminhee
Zenn
@hongminhee
Matrix
@hongminhee:matrix.org
X
@hongminhee

레포 파놨더니 모르는 사람들이 스타를 찍기 시작해서. 급하게 WIP라고 써놨다. 빨리 완성하자...

2

식탁보를 만들어감에 있어 좀 더 많은 분들께서 편하게 참여하실 수 있도록, Discord 서버를 새로 오픈했습니다. 많이 참여해주시고 알려주시면 감사하겠습니다!

https://discord.gg/eT2UnUXyTV

1

LLM 코딩의 부정적인 영향으로 우려하는 부분이 있는데, 점점 현실이 되어가는거 같다. 사람들이 코드를 안 읽고 너무 많이 짠다.

사실 오랫동안 많은곳에서 쓰이는 좋은 프로그램들은, 그 많은 코드를 짜서 어떻게든 동작시키게 만들었다는 것 이전에, 그냥 '뭘 만들지'에 대한 접근부터 훌륭한 경우가 많다(많은 UNIX 기본 프로그램들을 생각해보라). 옛날에는, 내가 어떤 기능 A가 필요할때, 내가 그 기능 A를 짜는게 힘든 일이니까 그 기능 A를 제공하는 프로그램을 먼저 찾게되고, 그러면 그 프로그램은 나의 근시안적인 접근보다 나은 더 좋은 개념을 갖고 있는 경우가 많았다. 그러면 나는 이제 그 개념을 익히며 내 자신을 업데이트하면 되는 것이다.

근데 자기 자신을 업데이트하는건 피곤한 일이라서, 사람들은 그걸 피할수 있는 기회를 놓치지 않는다. 그래서 현재 자기 수준에 맞는 평범한 프로그램을 만드는 길을(단돈 0.1$) 기꺼이 선택해버린다.

7
1
2

@shinspiegelJeferson 'Shin' It depends on the domain. For simple utilities, sure, 2–5 arguments work fine. But tools like git, docker, or kubectl have dozens of subcommands with their own options, and splitting them into separate binaries would hurt usability. In my case, Fedify's CLI has subcommands like init, lookup, inbox, each with multiple flags. Optique helps keep that manageable while maintaining type safety across the whole structure.

1
8
0
0

@shinspiegelJeferson 'Shin' That's a fair concern in general, but it applies to any dependency, not just CLI parsers. Optique's core package has zero runtime dependencies, which keeps the attack surface minimal. If supply chain security is a priority, you could also vendor the code or pin to specific versions. That said, manually parsing process.argv for anything beyond trivial cases tends to introduce its own bugs.

1

저는 유별난 Markdown 스타일을 고집하는데요, 그래서 어떠한 린트나 포매터도 제 요구 사항을 충족시키지 못해서 Markdown 문서에 한해서는 린트/포매터 없이 손으로 고치며 살고 있었습니다만… 오늘 갑자기 삘 받아서 바이브 코딩으로 markdownlint 커스텀 룰들을 구현했습니다. 아마도 이걸 필요로 하는 분들은 없겠지만…

https://github.com/dahlia/markdownlint-rules

5

I finally gave in and wrote my own markdownlint rules to enforce my peculiar and stubborn Markdown style. Probably no one else will ever need these, but I've published them as open source anyway.

https://github.com/dahlia/markdownlint-rules

0

LLM을 위한 cat, lat을 만들어볼까 생각이 들었다. 어떤 파일을 열던지 간에, 토큰 아끼고 LLM이 쉽게 읽도록 적당히 알아서 바꿔서 보여주는 것이다. 그리고 CLAUDE.md 같은거에 cat 대신에 쓰라고 하는거지.

처음엔 JSON 파일을 minify해서 토큰 아끼는 정도를 생각했는데, 막상 클로드한테 물어보니 자기도 들여쓰기가 있어야 읽기 편하다고 한다. 응? 그래서 쓸모있는 접근을 물어봤더니, 코드를 읽을때 앞에 함수 시그니쳐/클래스 정의 등의 요약을 달아주면 좋겠다고 한다.

쓸모있게 만드려면 좀더 고민해야할듯..

0

LLM을 위한 cat, lat을 만들어볼까 생각이 들었다. 어떤 파일을 열던지 간에, 토큰 아끼고 LLM이 쉽게 읽도록 적당히 알아서 바꿔서 보여주는 것이다. 그리고 CLAUDE.md 같은거에 cat 대신에 쓰라고 하는거지.

처음엔 JSON 파일을 minify해서 토큰 아끼는 정도를 생각했는데, 막상 클로드한테 물어보니 자기도 들여쓰기가 있어야 읽기 편하다고 한다. 응? 그래서 쓸모있는 접근을 물어봤더니, 코드를 읽을때 앞에 함수 시그니쳐/클래스 정의 등의 요약을 달아주면 좋겠다고 한다.

쓸모있게 만드려면 좀더 고민해야할듯..

2

Building CLI apps with TypeScript in 2026

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

We've all been there. You start a quick TypeScript CLI with process.argv.slice(2), add a couple of options, and before you know it you're drowning in if/else blocks and parseInt calls. It works, until it doesn't.

In this guide, we'll move from manual argument parsing to a fully type-safe CLI with subcommands, mutually exclusive options, and shell completion.

The naïve approach: parsing process.argv

Let's start with the most basic approach. Say we want a greeting program that takes a name and optionally repeats the greeting:

// greet.ts
const args = process.argv.slice(2);

let name: string | undefined;
let count = 1;

for (let i = 0; i < args.length; i++) {
  if (args[i] === "--name" || args[i] === "-n") {
    name = args[++i];
  } else if (args[i] === "--count" || args[i] === "-c") {
    count = parseInt(args[++i], 10);
  }
}

if (!name) {
  console.error("Error: --name is required");
  process.exit(1);
}

for (let i = 0; i < count; i++) {
  console.log(`Hello, ${name}!`);
}

Run node greet.js --name Alice --count 3 and you'll get three greetings.

But this approach is fragile. count could be NaN if someone passes --count foo, and we'd silently proceed. There's no help text. If someone passes --name without a value, we'd read the next option as the name. And the boilerplate grows fast with each new option.

The traditional libraries

You've probably heard of Commander.js and Yargs. They've been around for years and solve the basic problems:

// With Commander.js
import { program } from "commander";

program
  .requiredOption("-n, --name <n>", "Name to greet")
  .option("-c, --count <number>", "Number of times to greet", "1")
  .parse();

const opts = program.opts();

These libraries handle help text, option parsing, and basic validation. But they were designed before TypeScript became mainstream, and the type safety is bolted on rather than built in.

The real problem shows up when you need mutually exclusive options. Say your CLI works either in "server mode" (with --port and --host) or "client mode" (with --url). With these libraries, you end up with a config object where all options are potentially present, and you're left writing runtime checks to ensure the user didn't mix incompatible flags. TypeScript can't help you because the types don't reflect the actual constraints.

Enter Optique

Optique takes a different approach. Instead of configuring options declaratively, you build parsers by composing smaller parsers together. The types flow naturally from this composition, so TypeScript always knows exactly what shape your parsed result will have.

Optique works across JavaScript runtimes: Node.js, Deno, and Bun are all supported. The core parsing logic has no runtime-specific dependencies, so you can even use it in browsers if you need to parse CLI-like arguments in a web context.

Let's rebuild our greeting program:

import { object } from "@optique/core/constructs";
import { option } from "@optique/core/primitives";
import { integer, string } from "@optique/core/valueparser";
import { withDefault } from "@optique/core/modifiers";
import { run } from "@optique/run";

const parser = object({
  name: option("-n", "--name", string()),
  count: withDefault(option("-c", "--count", integer({ min: 1 })), 1),
});

const config = run(parser);
// config is typed as { name: string; count: number }

for (let i = 0; i < config.count; i++) {
  console.log(`Hello, ${config.name}!`);
}

Types are inferred automatically. config.name is string, not string | undefined. config.count is number, guaranteed to be at least 1. Validation is built in: integer({ min: 1 }) rejects non-integers and values below 1 with clear error messages. Help text is generated automatically, and the run() function handles errors and exits with appropriate codes.

Install it with your package manager of choice:

npm add @optique/core @optique/run
# or: pnpm add, yarn add, bun add, deno add jsr:@optique/core jsr:@optique/run

Building up: a file converter

Let's build something more realistic: a file converter that reads from an input file, converts to a specified format, and writes to an output file.

import { object } from "@optique/core/constructs";
import { optional, withDefault } from "@optique/core/modifiers";
import { argument, option } from "@optique/core/primitives";
import { choice, string } from "@optique/core/valueparser";
import { run } from "@optique/run";

const parser = object({
  input: argument(string({ metavar: "INPUT" })),
  output: option("-o", "--output", string({ metavar: "FILE" })),
  format: withDefault(
    option("-f", "--format", choice(["json", "yaml", "toml"])),
    "json"
  ),
  pretty: option("-p", "--pretty"),
  verbose: option("-v", "--verbose"),
});

const config = run(parser, {
  help: "both",
  version: { mode: "both", value: "1.0.0" },
});

// config.input: string
// config.output: string
// config.format: "json" | "yaml" | "toml"
// config.pretty: boolean
// config.verbose: boolean

The type of config.format isn't just string. It's the union "json" | "yaml" | "toml". TypeScript will catch typos like config.format === "josn" at compile time.

The choice() parser is useful for any option with a fixed set of valid values: log levels, output formats, environment names, and so on. You get both runtime validation (invalid values are rejected with helpful error messages) and compile-time checking (TypeScript knows the exact set of possible values).

Mutually exclusive options

Now let's tackle the case that trips up most CLI libraries: mutually exclusive options. Say our tool can either run as a server or connect as a client, but not both:

import { object, or } from "@optique/core/constructs";
import { withDefault } from "@optique/core/modifiers";
import { argument, constant, option } from "@optique/core/primitives";
import { integer, string, url } from "@optique/core/valueparser";
import { run } from "@optique/run";

const parser = or(
  // Server mode
  object({
    mode: constant("server"),
    port: option("-p", "--port", integer({ min: 1, max: 65535 })),
    host: withDefault(option("-h", "--host", string()), "0.0.0.0"),
  }),
  // Client mode
  object({
    mode: constant("client"),
    url: argument(url()),
  }),
);

const config = run(parser);

The or() combinator tries each alternative in order. The first one that successfully parses wins. The constant() parser adds a literal value to the result without consuming any input, which serves as a discriminator.

TypeScript infers a discriminated union:

type Config =
  | { mode: "server"; port: number; host: string }
  | { mode: "client"; url: URL };

Now you can write type-safe code that handles each mode:

if (config.mode === "server") {
  console.log(`Starting server on ${config.host}:${config.port}`);
} else {
  console.log(`Connecting to ${config.url.hostname}`);
}

Try accessing config.url in the server branch. TypeScript won't let you. The compiler knows that when mode is "server", only port and host exist.

This is the key difference from configuration-based libraries. With Commander or Yargs, you'd get a type like { port?: number; host?: string; url?: string } and have to check at runtime which combination of fields is actually present. With Optique, the types match the actual constraints of your CLI.

Subcommands

For larger tools, you'll want subcommands. Optique handles this with the command() parser:

import { object, or } from "@optique/core/constructs";
import { optional } from "@optique/core/modifiers";
import { argument, command, constant, option } from "@optique/core/primitives";
import { string } from "@optique/core/valueparser";
import { run } from "@optique/run";

const parser = or(
  command("add", object({
    action: constant("add"),
    key: argument(string({ metavar: "KEY" })),
    value: argument(string({ metavar: "VALUE" })),
  })),
  command("remove", object({
    action: constant("remove"),
    key: argument(string({ metavar: "KEY" })),
  })),
  command("list", object({
    action: constant("list"),
    pattern: optional(option("-p", "--pattern", string())),
  })),
);

const result = run(parser, { help: "both" });

switch (result.action) {
  case "add":
    console.log(`Adding ${result.key}=${result.value}`);
    break;
  case "remove":
    console.log(`Removing ${result.key}`);
    break;
  case "list":
    console.log(`Listing${result.pattern ? ` (filter: ${result.pattern})` : ""}`);
    break;
}

Each subcommand gets its own help text. Run myapp add --help and you'll see only the options relevant to add. Run myapp --help and you'll see a summary of all available commands.

The pattern here is the same as mutually exclusive options: or() to combine alternatives, constant() to add a discriminator. This consistency is one of Optique's strengths. Once you understand the basic combinators, you can build arbitrarily complex CLI structures by composing them.

Shell completion

Optique has built-in shell completion for Bash, zsh, fish, PowerShell, and Nushell. Enable it by passing completion: "both" to run():

const config = run(parser, {
  help: "both",
  version: { mode: "both", value: "1.0.0" },
  completion: "both",
});

Users can then generate completion scripts:

$ myapp --completion bash >> ~/.bashrc
$ myapp --completion zsh >> ~/.zshrc
$ myapp --completion fish > ~/.config/fish/completions/myapp.fish

The completions are context-aware. They know about your subcommands, option values, and choice() alternatives. Type myapp --format <TAB> and you'll see json, yaml, toml as suggestions. Type myapp a<TAB> and it'll complete to myapp add.

Completion support is often an afterthought in CLI tools, but it makes a real difference in user experience. With Optique, you get it essentially for free.

Integrating with validation libraries

Already using Zod for validation in your project? The @optique/zod package lets you reuse those schemas as CLI value parsers:

import { z } from "zod";
import { zod } from "@optique/zod";
import { option } from "@optique/core/primitives";

const email = option("--email", zod(z.string().email()));
const port = option("--port", zod(z.coerce.number().int().min(1).max(65535)));

Your existing validation logic just works. The Zod error messages are passed through to the user, so you get the same helpful feedback you're used to.

Prefer Valibot? The @optique/valibot package works the same way:

import * as v from "valibot";
import { valibot } from "@optique/valibot";
import { option } from "@optique/core/primitives";

const email = option("--email", valibot(v.pipe(v.string(), v.email())));

Valibot's bundle size is significantly smaller than Zod's (~10KB vs ~52KB), which can matter for CLI tools where startup time is noticeable.

Tips

A few things I've learned building CLIs with Optique:

Start simple. Begin with object() and basic options. Add or() for mutually exclusive groups only when you need them. It's easy to over-engineer CLI parsers.

Use descriptive metavars. Instead of string(), write string({ metavar: "FILE" }) or string({ metavar: "URL" }). The metavar appears in help text and error messages, so it's worth the extra few characters.

Leverage withDefault(). It's better than making options optional and checking for undefined everywhere. Your code becomes cleaner when you can assume values are always present.

Test your parser. Optique's core parsing functions work without process.argv, so you can unit test your parser logic:

import { parse } from "@optique/core/parser";

const result = parse(parser, ["--name", "Alice", "--count", "3"]);
if (result.success) {
  assert.equal(result.value.name, "Alice");
  assert.equal(result.value.count, 3);
}

This is especially valuable for complex parsers with many edge cases.

Going further

We've covered the fundamentals, but Optique has more to offer:

  • Async value parsers for validating against external sources, like checking if a Git branch exists or if a URL is reachable
  • Path validation with path() for checking file existence, directory structure, and file extensions
  • Custom value parsers for domain-specific types (though Zod/Valibot integration is usually easier)
  • Reusable option groups with merge() for sharing common options across subcommands
  • The @optique/temporal package for parsing dates and times using the Temporal API

Check out the documentation for the full picture. The tutorial walks through the concepts in more depth, and the cookbook has patterns for common scenarios.

That's it

Building CLIs in TypeScript doesn't have to mean fighting with types or writing endless runtime validation. Optique lets you express constraints in a way that TypeScript actually understands, so the compiler catches mistakes before they reach production.

The source is on GitHub, and packages are available on both npm and JSR.


Questions or feedback? Find me on the fediverse or open an issue on the GitHub repo.

Read more →
1

LLM에서 마크다운이 널리 쓰이게 되면서 안 보고 싶어도 볼 수 밖에 없게 된 흔한 꼬라지로 그림에서 보는 것처럼 마크다운 강조 표시(**)가 그대로 노출되어 버리는 광경이 있다. 이 문제는 CommonMark의 고질적인 문제로, 한 10년 전쯤에 보고한 적도 있는데 지금까지 어떤 해결책도 제시되지 않은 채로 방치되어 있다.

문제의 상세는 이러하다. CommonMark는 마크다운을 표준화하는 과정에서 파싱의 복잡도를 제한하기 위해 연속된 구분자(delimiter run)라는 개념을 넣었는데, 연속된 구분자는 어느 방향에 있느냐에 따라서 왼편(left-flanking)과 오른편(right-flanking)이라는 속성을 가질 수 있다(왼편이자 오른편일 수도 있고, 둘 다 아닐 수도 있다). 이 규칙에 따르면 **는 왼편의 연속된 구분자로부터 시작해서 오른편의 연속된 구분자로 끝나야만 한다. 여기서 중요한 건 왼편인지 오른편인지를 판단하는 데 외부 맥락이 전혀 안 들어가고 주변의 몇 글자만 보고 바로 결정된다는 것인데, 이를테면 왼편의 연속된 구분자는 **<보통 글자> 꼴이거나 <공백>**<기호> 또는 <기호>**<기호> 꼴이어야 한다. ("보통 글자"란 공백이나 기호가 아닌 글자를 가리킨다.) 첫번째 꼴은 아무래도 **마크다운**은 같이 낱말 안에 끼어 들어가 있는 연속된 구분자를 허용하기 위한 것이고, 두번째/세번째 꼴은 이 **"마크다운"** 형식은 같이 기호 앞에 붙어 있는 연속된 구분자를 제한적으로 허용하기 위한 것이라 해석할 수 있겠다. 오른편도 방향만 다르고 똑같은 규칙을 가지는데, 이 규칙으로 **마크다운(Markdown)**은을 해석해 보면 뒷쪽 **의 앞에는 기호가 들어 있으므로 뒤에는 공백이나 기호가 나와야 하지만 보통 글자가 나왔으므로 오른편이 아니라고 해석되어 강조의 끝으로 처리되지 않는 것이다.

CommonMark 명세에서도 설명되어 있지만, 이 규칙의 원 의도는 **이런 **식으로** 중첩되어** 강조된 문법을 허용하기 위한 것이다. 강조를 한답시고 **이런 ** 식으로 공백을 강조 문법 안쪽에 끼워 넣는 일이 일반적으로는 없으므로, 이런 상황에서 공백에 인접한 강조 문법은 항상 특정 방향에만 올 수 있다고 선언하는 것으로 모호함을 해소하는 것이다. 허나 CJK 환경에서는 공백이 아예 없거나 공백이 있어도 한국어처럼 낱말 안에서 기호를 쓰는 경우가 드물지 않기 때문에, 이런 식으로 어느 연속된 구분자가 왼편인지 오른편인지 추론하는 데 한계가 있다는 것이다. 단순히 <보통 문자>**<기호>도 왼편으로 해석하는 식으로 해서 **마크다운(Markdown)**은 같은 걸 허용한다 하더라도, このような**[状況](...)**は 이런 상황은 어쩔 것인가? 내가 느끼기에는 중첩되어 강조된 문법의 효용은 제한적인 반면 이로 인해 생기는 CJK 환경에서의 불편함은 명확하다. 그리고 LLM은 CommonMark의 설계 의도 따위는 고려하지 않고 실제 사람들이 사용할 법한 식으로 마크다운을 쓰기 때문에, 사람들이 막연하게 가지고만 있던 이런 불편함이 그대로 표면화되어 버린 것이고 말이다.

* 21. Ba5# - 백이 룩과 퀸을 희생한 후, 퀸 대신 **비숍(Ba5)**이 결정적인 체크메이트를 성공시킵니다. 흑 킹이 탈출할 곳이 없으며, 백의 기물로 막을 수도 없습니다. [강조 처리된 "비숍(Ba5)" 앞뒤에 마크다운의 강조 표시 "**"가 그대로 노출되어 있다.]

As Markdown has become the standard for LLM outputs, we are now forced to witness a common and unsightly mess where Markdown emphasis markers (**) remain unrendered and exposed, as seen in the image. This is a chronic issue with the CommonMark specification---one that I once reported about ten years ago---but it has been left neglected without any solution to this day.

The technical details of the problem are as follows: In an effort to limit parsing complexity during the standardization process, CommonMark introduced the concept of "delimiter runs." These runs are assigned properties of being "left-flanking" or "right-flanking" (or both, or neither) depending on their position. According to these rules, a bolded segment must start with a left-flanking delimiter run and end with a right-flanking one. The crucial point is that whether a run is left- or right-flanking is determined solely by the immediate surrounding characters, without any consideration of the broader context. For instance, a left-flanking delimiter must be in the form of **<ordinary character>, <whitespace>**<punctuation>, or <punctuation>**<punctuation>. (Here, "ordinary character" refers to any character that is not whitespace or punctuation.) The first case is presumably intended to allow markers embedded within a word, like **마크다운**은, while the latter cases are meant to provide limited support for markers placed before punctuation, such as in 이 **"마크다운"** 형식은. The rules for right-flanking are identical, just in the opposite direction.

However, when you try to parse a string like **마크다운(Markdown)**은 using these rules, it fails because the closing ** is preceded by punctuation (a parenthesis) and it must be followed by whitespace or another punctuation mark to be considered right-flanking. Since it is followed by an ordinary letter (), it is not recognized as right-flanking and thus fails to close the emphasis.

As explained in the CommonMark spec, the original intent of this rule was to support nested emphasis, like **this **way** of nesting**. Since users typically don't insert spaces inside emphasis markers (e.g., **word **), the spec attempts to resolve ambiguity by declaring that markers adjacent to whitespace can only function in a specific direction. However, in CJK (Chinese, Japanese, Korean) environments, either spaces are completly absent or (as in Korean) punctuations are commonly used within a word. Consequently, there are clear limits to inferring whether a delimiter is left or right-flanking based on these rules. Even if we were to allow <ordinary character>**<punctuation> to be interpreted as left-flanking to accommodate cases like **마크다운(Markdown)**은, how would we handle something like このような**[状況](...)は**?

In my view, the utility of nested emphasis is marginal at best, while the frustration it causes in CJK environments is significant. Furthermore, because LLMs generate Markdown based on how people would actually use it---rather than strictly following the design intent of CommonMark---this latent inconvenience that users have long felt is now being brought directly to the surface.

* 21. Ba5# - 백이 룩과 퀸을 희생한 후, 퀸 대신 **비숍(Ba5)**이 결정적인 체크메이트를 성공시킵니다. 흑 킹이 탈출할 곳이 없으며, 백의 기물로 막을 수도 없습니다. [The emphasized portion `비숍(Ba5)` is surrounded by unrendered Markdown emphasis marks `**`.]
14
2
1
2

LLM에서 마크다운이 널리 쓰이게 되면서 안 보고 싶어도 볼 수 밖에 없게 된 흔한 꼬라지로 그림에서 보는 것처럼 마크다운 강조 표시(**)가 그대로 노출되어 버리는 광경이 있다. 이 문제는 CommonMark의 고질적인 문제로, 한 10년 전쯤에 보고한 적도 있는데 지금까지 어떤 해결책도 제시되지 않은 채로 방치되어 있다.

문제의 상세는 이러하다. CommonMark는 마크다운을 표준화하는 과정에서 파싱의 복잡도를 제한하기 위해 연속된 구분자(delimiter run)라는 개념을 넣었는데, 연속된 구분자는 어느 방향에 있느냐에 따라서 왼편(left-flanking)과 오른편(right-flanking)이라는 속성을 가질 수 있다(왼편이자 오른편일 수도 있고, 둘 다 아닐 수도 있다). 이 규칙에 따르면 **는 왼편의 연속된 구분자로부터 시작해서 오른편의 연속된 구분자로 끝나야만 한다. 여기서 중요한 건 왼편인지 오른편인지를 판단하는 데 외부 맥락이 전혀 안 들어가고 주변의 몇 글자만 보고 바로 결정된다는 것인데, 이를테면 왼편의 연속된 구분자는 **<보통 글자> 꼴이거나 <공백>**<기호> 또는 <기호>**<기호> 꼴이어야 한다. ("보통 글자"란 공백이나 기호가 아닌 글자를 가리킨다.) 첫번째 꼴은 아무래도 **마크다운**은 같이 낱말 안에 끼어 들어가 있는 연속된 구분자를 허용하기 위한 것이고, 두번째/세번째 꼴은 이 **"마크다운"** 형식은 같이 기호 앞에 붙어 있는 연속된 구분자를 제한적으로 허용하기 위한 것이라 해석할 수 있겠다. 오른편도 방향만 다르고 똑같은 규칙을 가지는데, 이 규칙으로 **마크다운(Markdown)**은을 해석해 보면 뒷쪽 **의 앞에는 기호가 들어 있으므로 뒤에는 공백이나 기호가 나와야 하지만 보통 글자가 나왔으므로 오른편이 아니라고 해석되어 강조의 끝으로 처리되지 않는 것이다.

CommonMark 명세에서도 설명되어 있지만, 이 규칙의 원 의도는 **이런 **식으로** 중첩되어** 강조된 문법을 허용하기 위한 것이다. 강조를 한답시고 **이런 ** 식으로 공백을 강조 문법 안쪽에 끼워 넣는 일이 일반적으로는 없으므로, 이런 상황에서 공백에 인접한 강조 문법은 항상 특정 방향에만 올 수 있다고 선언하는 것으로 모호함을 해소하는 것이다. 허나 CJK 환경에서는 공백이 아예 없거나 공백이 있어도 한국어처럼 낱말 안에서 기호를 쓰는 경우가 드물지 않기 때문에, 이런 식으로 어느 연속된 구분자가 왼편인지 오른편인지 추론하는 데 한계가 있다는 것이다. 단순히 <보통 문자>**<기호>도 왼편으로 해석하는 식으로 해서 **마크다운(Markdown)**은 같은 걸 허용한다 하더라도, このような**[状況](...)**は 이런 상황은 어쩔 것인가? 내가 느끼기에는 중첩되어 강조된 문법의 효용은 제한적인 반면 이로 인해 생기는 CJK 환경에서의 불편함은 명확하다. 그리고 LLM은 CommonMark의 설계 의도 따위는 고려하지 않고 실제 사람들이 사용할 법한 식으로 마크다운을 쓰기 때문에, 사람들이 막연하게 가지고만 있던 이런 불편함이 그대로 표면화되어 버린 것이고 말이다.

* 21. Ba5# - 백이 룩과 퀸을 희생한 후, 퀸 대신 **비숍(Ba5)**이 결정적인 체크메이트를 성공시킵니다. 흑 킹이 탈출할 곳이 없으며, 백의 기물로 막을 수도 없습니다. [강조 처리된 "비숍(Ba5)" 앞뒤에 마크다운의 강조 표시 "**"가 그대로 노출되어 있다.]
14
1
0

@mitsuhikoArmin Ronacher @glyph

That response sounds like you are admitting that LLMs are just repackaging human work against the will of those humans?

Repackaging someone else's work and putting your name on it is not sharing, it's stealing labour and stealing credit. Pirates don't pretend they made something, but LLM users do pretend this.

I agree 70 years is too long, but LLMs are reducing that to 0 years while also erasing from history the creator's credit.

Open source is where the creators are credited and they have consented to their work being used by others. LLMs are removing credit and doing it without consent.

@FediThingFediThing 🏳️‍🌈 @glyph LLMs are trained on human-created material in much the same way a person learns by reading books and then acting on what they've learned. They don't directly reproduce that material.

As I mentioned I strongly believe that broad sharing of knowledge is a net benefit to humanity. Questions of credit and attribution are a separate issue and to discuss them meaningfully, you first have to be clear about what you consider reasonable attribution in the first place.

You can take for instance the tankgame and then tell me which part should be attributed and is not, and what you would be attributing it to.

On the "against the will": I want you to use the code I wrote, it's definitely not against my will that LLMs are trained on the code I wrote over the years.

0

0.9.0 is here!

This release brings /await support to parsers. Now you can validate input against external resources—databases, APIs, Git repositories—directly at parse time, with full type safety.

The new @optique/git package showcases this: validate branch names, tags, and commit SHAs against an actual Git repo, complete with shell completion suggestions.

Other highlights:

  • Hidden option support for deprecated/internal flags
  • Numeric choices in choice()
  • Security fix for shell completion scripts

Fully backward compatible—your existing parsers work unchanged.

https://github.com/dahlia/optique/discussions/75

1

My last salaried job was at a company that built blockchain technology. No, it wasn't for cryptocurrency. The goal was to use blockchain to create a fully peer-to-peer, decentralized game. I found it a technically interesting goal. I've always been fascinated by decentralized technologies, which is also why I'm drawn to ActivityPub. Another thing that attracted me was the promise that this technology would be implemented as 100% open source. I had always wanted to work on open source full-time, so I accepted the offer.

However, once I started working there, I found myself increasingly disappointed. The organization gradually filled up with so-called “crypto bros,” and the culture shifted toward prioritizing token price over technical achievement. I and a few close colleagues believed that introducing partial centralization to the fully decentralized system—whether to defend the token price or to rush a release—was not a “minor compromise” but a “major corruption.” The rest of the organization didn't see it that way.

One of the most painful things about being in that organization was the fact that the technology I was creating was not only unhelpful to society, but was actually harming the environment and society. At the time, I felt like I was working for a tobacco company—knowing that cigarettes harm people's health, yet turning a blind eye and doing the job anyway.

I'm no fan of cryptocurrency, but I still think blockchain has technically interesting aspects. However, blockchain has already become socially inseparable from cryptocurrency, and even if blockchain is technically interesting, there are very few domains where it's actually useful. Furthermore, the negative environmental impact of blockchain technology is a problem that must be solved for it to be taken seriously. In its current state, when I weigh the harm against the utility, I believe the harm overwhelmingly outweighs it.

Anyway, I have now completely said goodbye to blockchain technology. I feel at ease now that I don't have to live with that guilt anymore. I also came to realize that engineers must consider not only the technical interest of a technology but also its social impact. So for now, I want to focus on ActivityPub. I find it both technically interesting and socially meaningful!

1
0

클로드 코드 쓰고있으니 더 나은 VCS에 대한 욕심이 커진다. 나는 클로드가 브랜치를 더 자주 쪼개서, 원하는 시점으로의 롤백이 더 편해졌으면 좋겠다.

하나 생각나는 아이디어는 브랜치 명을 hierachial하게 만들어서 가령 fix-bug-1/refactor-class-foo/fix-function-bar 이런식으로, 무슨 일하는지의 맥락을 브랜치명에 나타내는 것이다. 그리고 a/b 브랜치는 a 브랜치의 자식이어야 한다는 제약도 강제한다.

1
4
1
3
0

고등학생 때부터 Vim을 썼으니까, Vim/Neovim을 합치면 거의 15년 가까이 썼던 것 같다. 그러다가 Deno와 TypeScript를 접하면서 Visual Studio Code로 갈아탔는데, 그러고 한 2–3년? Zed가 나와서 Zed를 또 1년 가까이 썼다. (아, VS Code를 쓸 때도 Zed를 쓸 때도 Vim 키 바인딩을 끄지는 못 했다.)

그런데 요즘에는 Claude Code니 OpenCode니 LLM 기반의 코딩 에이전트들을 꽤 열심히 쓰게 되면서 에디터 자체를 잘 안 쓰게 되었다. 심지어 import 한 줄 추가하는 것도 프롬프트로 해결하게 된다. 그래야 LLM한테 맥락이 주어져서 혼선이 없기 때문이다. (내가 말 없이 코드를 고쳐 두면 LLM이 뭔가의 이상 상황으로 받아들이거나, 무심코 원래 코드로 되돌리기도 한다.) 그러다 보니 커밋 직전에 디테일을 손 보거나 코드를 리뷰할 때 빼고는 에디터를 잘 안 켜게 된다. 켜더라도 즉각적으로 열리는 걸 선호하게 되어서, Vim/Neovim이 가장 먼저 손이 가더라.

결국에는 몇 년 동안의 방황을 거쳐 다시 Vim/Neovim으로 돌아오게 되었다는 이야기. 그래서 조만간 먼지가 쌓인 Vim/Neovim 설정도 새해 맞이를 겸해서 한 번 청소를 해야겠다 싶다.

1
2

I wrote Zig bindings to quickjs-ng with 96% API coverage (~240 exported C decls) with unit tests, examples, and doc strings on all functions in less than 6 total hours with AI assistance. I never want to hear that AI isn't faster ever again. github.com/mitchellh/zig-quick

This isn't slop. I worked for those 6 hours.

I was reviewing everything it outputted, updating my AGENTS.md to course correct future work, ensuring the output was idiomatic Zig, writing my own tests on the side to verify its work (while it worked), and more. My work was split across ~40 separate Amp threads (not one mega session, which doesn't work anyways unless you're orchestrating).

I have a ton of experience writing bindings to libraries for various languages, especially Zig. I have never achieved this much coverage in so little time with such high quality (e.g. test coverage). My usual approach is to get bind just-enough of the surface area to do my actual work and move on. This time I thought I'd draw the whole owl, because it's a new world. And I'm very happy with the result.

Anyone with experience writing bindings knows that you do some small surface area, then the rest of the coverage is annoying repetition. That's why I usually stopped. Well, LLMs/agents are really, really good at annoying repetition and pattern matching. So going from 5% API coverage to 95% is... cake.

There is probably some corners that are kind of nasty still, but I've been re-reviewing every line of code manually and there is nothing major. Definitely some areas that can just use a nicer Zig interfaces over the C API, but that's about it.

I plan on writing a longer form blog showcasing my threads, but you can at least see the final AGENTS.md I produced in the linked repo.

github.com/mitchellh/zig-quick

I will repeat that I was not sitting back at all during those 6 hours. While agents were working, I was working, just on separate -- but related -- tasks. I know for a fact that I could not have completed this amount of work in 6 hours fully manually (based on the experience that I've written something like 30+ bindings to C libraries in the past decade, probably more).

1

I wrote Zig bindings to quickjs-ng with 96% API coverage (~240 exported C decls) with unit tests, examples, and doc strings on all functions in less than 6 total hours with AI assistance. I never want to hear that AI isn't faster ever again. github.com/mitchellh/zig-quick

This isn't slop. I worked for those 6 hours.

I was reviewing everything it outputted, updating my AGENTS.md to course correct future work, ensuring the output was idiomatic Zig, writing my own tests on the side to verify its work (while it worked), and more. My work was split across ~40 separate Amp threads (not one mega session, which doesn't work anyways unless you're orchestrating).

I have a ton of experience writing bindings to libraries for various languages, especially Zig. I have never achieved this much coverage in so little time with such high quality (e.g. test coverage). My usual approach is to get bind just-enough of the surface area to do my actual work and move on. This time I thought I'd draw the whole owl, because it's a new world. And I'm very happy with the result.

Anyone with experience writing bindings knows that you do some small surface area, then the rest of the coverage is annoying repetition. That's why I usually stopped. Well, LLMs/agents are really, really good at annoying repetition and pattern matching. So going from 5% API coverage to 95% is... cake.

There is probably some corners that are kind of nasty still, but I've been re-reviewing every line of code manually and there is nothing major. Definitely some areas that can just use a nicer Zig interfaces over the C API, but that's about it.

I plan on writing a longer form blog showcasing my threads, but you can at least see the final AGENTS.md I produced in the linked repo.

github.com/mitchellh/zig-quick

1
1

Heya! I just released XenoAtom.Terminal github.com/XenoAtom/XenoAtom.T, a modern replacement for System.Console for .NET CLI/TUI apps. 🎉

It keeps a familiar Console-like feel, but adds the terminal-native stuff System.Console doesn't cover well: ANSI/VT styling + markup, unified async input events (keys/resize/mouse/paste), restore-on-dispose scopes (raw/cbreak, alternate screen, hide cursor…), clipboard, a rich ReadLine editor, & testable backends, built on top of XenoAtom.Ansi ✨

1

고등학생 때부터 Vim을 썼으니까, Vim/Neovim을 합치면 거의 15년 가까이 썼던 것 같다. 그러다가 Deno와 TypeScript를 접하면서 Visual Studio Code로 갈아탔는데, 그러고 한 2–3년? Zed가 나와서 Zed를 또 1년 가까이 썼다. (아, VS Code를 쓸 때도 Zed를 쓸 때도 Vim 키 바인딩을 끄지는 못 했다.)

그런데 요즘에는 Claude Code니 OpenCode니 LLM 기반의 코딩 에이전트들을 꽤 열심히 쓰게 되면서 에디터 자체를 잘 안 쓰게 되었다. 심지어 import 한 줄 추가하는 것도 프롬프트로 해결하게 된다. 그래야 LLM한테 맥락이 주어져서 혼선이 없기 때문이다. (내가 말 없이 코드를 고쳐 두면 LLM이 뭔가의 이상 상황으로 받아들이거나, 무심코 원래 코드로 되돌리기도 한다.) 그러다 보니 커밋 직전에 디테일을 손 보거나 코드를 리뷰할 때 빼고는 에디터를 잘 안 켜게 된다. 켜더라도 즉각적으로 열리는 걸 선호하게 되어서, Vim/Neovim이 가장 먼저 손이 가더라.

결국에는 몇 년 동안의 방황을 거쳐 다시 Vim/Neovim으로 돌아오게 되었다는 이야기. 그래서 조만간 먼지가 쌓인 Vim/Neovim 설정도 새해 맞이를 겸해서 한 번 청소를 해야겠다 싶다.

11
1

IPv6가 30주년을 맞았지만 여전히 세계를 장악하지 못한 이유
------------------------------
- 1995년 등장한
IPv6 는 32비트에서 128비트로 확장된 주소 체계를 통해 *인터넷 주소 고갈 문제* 를 해결하려 했음
- 그러나 *IPv4와의 비호환성* , *기능적 차별성 부족* , *NAT의 확산* 등으로 인해 전환이 지연됨
- 전문가들은 *배포 비용과 복잡성* , *ROI 부족* , *성능 불일치* 등이 여전히 주요 …
------------------------------
https://news.hada.io/topic?id=25533&utm_source=googlechat&utm_medium=bot&utm_campaign=1834

3

프로그래밍 언어 문법을 만들때, 비교 연산자에 <, <=, >, >= 등이 있는데, 어차피 좌우 순서만 바꾸면 되니까, >, >= 같은걸 그냥 압수하고 <, <=만 쓰게 한다음에 >, >= 요건 다른 용도로 쓰면 어떨까하는 생각이 듬.

4

@hongminhee洪 民憙 (Hong Minhee) 해커즈 퍼브에서 "그냥" 댓글을 막을 수 있게 해 주셨으면 합니다.

  • 어떤 글에 대해서 댓글을 안 받고 싶다면, 안 받을 권리는 글쓴이에게 있어야 한다고 생각합니다.
  • 댓글은 '내' 글을 남에게 보여줄 때 반드시 함께 노출되어 버리는데, '내'가 전혀 통제할 수 없습니다.
  • 댓글 그 자체가 사실 별로 책임 있는 소통에 적합한 수단이 아니라고 감히 주장하겠습니다. 한국어 언어 생활만 봐도, '악플'은 있어도 '악글'이나 '악영상' 등은 없습니다. 유독 댓글에 대해서만 따로 '악플'이라는 조어가 등장할 만큼, 댓글 쪽이 가장 더럽다는 것을 언중도 인식하고 있는 것입니다. 이것은 댓글에 대해서만 '남의 집'이라는 인식이 적용되는 인간 심리 때문입니다. 실제로 네이버 뉴스의 헤비 악플러들을 보면, 자기 블로그를 만들어서 그 온갖 입에 담지 못할 모욕적인 말을 늘어놓는 사람은 사실상 없다시피 하고, 꼭 댓글에만 그 짓을 하고 다니죠. 자기 집 안방, 자기 집 대문, 자기 간판, 자기 얼굴에다 똥을 싸기는 싫은 거죠. 즉 이것은 댓글 그 자체의 매체적 특성에서 온다는 것입니다.
  • 물론 모든 분들이 "댓글 그 자체가 나쁜 문명" 이라는 제 의견에 동의하실 필요는 없습니다만, 적어도 댓글 거부권은 개별적으로 존중되는 것이 맞다고 생각합니다. 그리고 많은 플랫폼이 이미 이것을 지원하고 있습니다(트위터, 유튜브, 네이버 블로그 등).

현재 액티비티퍼브 프로토콜에서 '댓글 안 받기' 관련 지원이 아직 부족한 것으로 보이기는 합니다. 하지만, 이 문제는 어차피 프로토콜에서 완벽한 해법을 도출할 수 없습니다. 예를 들어 악의적인 인스턴스가 내 글을 가져가서 악플을 주렁주렁 달며 조리돌림을 하려 한다 해도, 기술적으로 그걸 사전에 원천 차단할 방법은 없겠죠. 내가 '댓글 안 받기' 설정을 하든 말든, 저쪽 인스턴스에서 무시할 수 있습니다.

더 나아가 생각해 보면, 내 글을 연합우주도 아닌 그냥 다른 '사이트'에서 링크 가져간 뒤에, 자기들끼리 조롱하고 능욕하는 일도 얼마든지 있을 수 있는데, 그것도 글을 '공개'하는 이상 원천적으로 막는 것은 불가능할 것입니다.

다만 댓글이 '내' 글의 하단에 직접 박히는 것은 '내'가 거부할 수 있어야 한다고 생각합니다. 따라서 완벽한 해법을 기다릴 것이 아니라(어차피 불가능하므로), 그냥 지금 당장 '내' 글의 하단에 댓글이 달리는 것을 막는 간단한 구현부터 적용하는 것이, 맞는 접근이라고 생각합니다. 해커즈 퍼브에 이것을 구현해 주셨으면 합니다.

@xtjuxtapose 의견 감사합니다. 댓글 거부 기능의 필요성에 대해서는 저도 공감합니다. 말씀하신 대로 글쓴이가 자기 글 하단에 무엇이 노출될지 통제할 수 있어야 한다는 건 합리적인 주장이고, 실제로 많은 플랫폼이 이 기능을 제공하고 있기도 하고요.

다만 현실적인 상황을 말씀드리자면, Hackers' Pub은 제가 전업으로 개발하는 프로젝트가 아니다 보니, 지금 당장 이 기능을 최우선으로 구현하기가 어렵습니다. (실은 이 기능만 그런 게 아니라 Hackers' Pub 개발 전반에 제가 참여를 거의 못하고 있습니다—오히려 다른 분들께서 기여를 해주고 계세요.) 이 기능을 안 하겠다는 게 아니라, 바로 지금은 어렵다는 점 양해 부탁드립니다. GitHub 이슈로 남겨주시면 로드맵에 참고하겠습니다.

한편 Hackers' Pub은 오픈 소스 프로젝트이기도 해서, 혹시 직접 구현에 관심이 있으시다면 해당 PR은 최우선으로 리뷰하겠습니다. 코드베이스가 익숙하지 않으시더라도 어디서부터 손대면 좋을지, 어떤 식으로 구현하면 될지 등 제가 적극적으로 도와드릴 수 있으니 편하게 말씀해 주세요.

2
1
0
1
1

洪 民憙 (Hong Minhee) shared the below article:

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #5-3 DHCP

자손킴 @jasonkim@hackers.pub

DHCP(Dynamic Host Configuration Protocol)

DHCP는 IP주소, 서브넷 마스크, 기본 게이트웨이와 DNS 서버의 IP 주소 등 네트워크에 접속하기 위해 필요한 설정을 배포하는 프로토콜이다. RFC2131 에서 표준화 되어 있으며, DHCP의 역할, 메시지 형식, 형식을 구성하는 필드의 의미와 처리 흐름등이 상세하게 정의 되어 있다.

책에서는 DHCP를 L7에서 다루고 있는데, 정리하다보니 이게 L7에 있는게 맞나 싶은 생각이 든다.

IP 할당 방식

IP 주소를 단말(NIC)에 할당하는 방법에는 크게 '정적 할당'과 '동적 할당' 두 가지가 있다.

정적 할당

단말에 대해 일일이 수동으로 IP 주소를 설정하는 방식이다. 시스템 관리자가 비어있는 IP 주소를 사용자에게 할당해 준다.

정적 할당은 단말과 IP 주소가 고유하게 매핑되기 때문에 IP 주소 관리가 용이하다. 특정 IP의 이상 징후가 발생하면 어떤 단말이 문제인지 즉시 파악 할 수 있다. 그러나 단말의 수가 많아 지거나 교체가 빈번하면 하나씩 관리하기 어렵다는 문제가 있다.

동적 할당

DHCP를 사용하여 단말에 자동으로 IP 주소를 설정하는 방법이다. 정적 할당은 사용자가 시스템 관리자에게 요청하여 빈 IP 주소를 지급받아 수동 설정 했다면, 동적 할당은 이 모든 과정을 DHCP가 자동으로 처리한다.

DHCP 메시지 형식

DHCP는 UDP/67로 캡슐화된 DHCP 메시지 부분에 설정 정보를 담는다. DHCP 메시지는 여러 가지 필드로 구성되는데, 다음 3가지가 특히 중요하다.

  • 할당 클라이언트 IP 주소
    • DHCP 서버에서 단말에 배포하는 IP 주소
  • 클라이언트 MAC 주소
    • 단말의 MAC 주소
  • 옵션
    • 네트워크 설정에 관한 다양한 정보

옵션은 옵션 코드에 의해 식별되는데, 대표적인 코드는 다음과 같다.

  • (1) 서브넷 마스크
  • (3) 기본 게이트웨이
  • (6) DNS 서버 IP 주소
  • (12) 호스트 이름
  • (42) NTP 서버의 IP 주소
  • (51) IP 주소 임대 시간
  • (53) DHCP 메시지 유형
  • (54) DHCP 서버 ID

DHCP 처리 흐름

DHCP는 서버와 클라이언트로 구성되어 있다. DHCP 클라이언트가 있는 단말은 초기에는 IP가 할당되지 않은 상태이기 때문에 브로트캐스트를 통해 정보를 주고 받는다.

  1. DHCP Discover: 클라이언트가 네트워크에 접속하면 DHCP 서버를 찾기 위해 브로드캐스트로 Discover 메시지를 전송한다. 이 시점에서 클라이언트는 아직 IP 주소가 없으므로 출발지 IP는 0.0.0.0으로 설정된다.
  2. DHCP Offer: DHCP 서버가 Discover 메시지를 수신하면 할당 가능한 IP 주소와 서브넷 마스크, 임대 시간 등의 설정 정보를 담아 Offer 메시지를 유니캐스트로 응답한다. 네트워크에 여러 DHCP 서버가 존재하는 경우 클라이언트는 복수의 Offer를 수신할 수 있다.
  3. DHCP Request: 클라이언트가 수신한 Offer 중 하나를 선택하여 해당 DHCP 서버에 IP 주소 할당을 정식으로 요청한다. 이 메시지도 브로드캐스트로 전송되며, 선택되지 않은 다른 DHCP 서버들에게 해당 Offer가 거절되었음을 알리는 역할도 한다.
  4. DHCP ACK: DHCP 서버가 Request를 승인하면 ACK 메시지를 전송하여 IP 주소 할당을 확정한다. 클라이언트는 이 메시지를 수신한 후 비로소 할당받은 IP 주소를 사용할 수 있게 된다. 만약 요청한 IP 주소를 할당할 수 없는 경우에는 DHCP NAK 메시지가 전송된다.
  5. DHCP Release: 클라이언트가 더 이상 IP 주소를 사용하지 않을 때 서버에 반환을 알리는 메시지이다. 이 메시지를 수신한 서버는 해당 IP 주소를 풀(Pool)에 반환하여 다른 클라이언트가 사용할 수 있도록 한다.

IP 주소 임대와 갱신

DHCP로 할당받은 IP 주소는 영구적인 것이 아니라 임대 시간(Lease Time)이 정해져 있다. 클라이언트는 임대 시간이 만료되기 전에 갱신을 요청해야 하는데, 일반적으로 임대 시간의 절반(50%)이 경과하면 DHCP Request 메시지를 서버에 유니캐스트로 전송하여 갱신을 시도한다. 이 갱신이 실패하면 임대 시간의 7/8(87.5%)가 경과한 시점에 다시 브로드캐스트로 갱신을 시도하며, 그래도 실패하면 임대 만료 시 IP 주소 사용을 중단하고 처음부터 Discover 과정을 다시 수행한다.

네트워크 인프라 자동화의 기반이 되는 DHCP

DHCP는 IP 주소 할당 외에도 옵션 필드를 활용하여 다양한 부가 기능을 제공할 수 있다. 그 중 대표적인 것이 네트워크 부팅(PXE) 지원이다.

  • (66) TFTP 서버 이름
    • 부트 파일을 제공하는 TFTP 서버의 IP 주소 또는 호스트명
  • (67) 부트 파일 이름
    • 클라이언트가 다운로드해야 할 네트워크 부트 프로그램의 경로

PXE 부팅

PXE(Preboot Execution Environment)는 로컬 저장 장치 없이 네트워크를 통해 운영체제를 부팅하는 기술이다. PXE를 사용하면 서버의 NIC가 네트워크에서 부팅 이미지를 받아와 자동으로 OS 설치를 진행할 수 있다.

PXE 부팅의 동작 흐름은 다음과 같다.

  1. 클라이언트가 전원을 켜면 NIC의 PXE 펌웨어가 DHCP Discover 메시지를 브로드캐스트한다.
  2. DHCP 서버는 IP 주소와 함께 옵션 66(TFTP 서버 주소)과 옵션 67(부트 파일 경로)을 응답한다.
  3. 클라이언트는 TFTP 프로토콜을 사용하여 지정된 서버에서 부트 파일(예: pxelinux.0, bootx64.efi)을 다운로드한다.
  4. 다운로드한 부트 로더가 실행되어 OS 설치 또는 부팅이 진행된다.

이처럼 DHCP의 옵션 필드를 활용하면 단순한 IP 할당을 넘어 네트워크 인프라 자동화의 기반을 구축할 수 있다.

Read more →
1

洪 民憙 (Hong Minhee) shared the below article:

도커로 구축한 랩에서 혼자 실습하며 배우는 네트워크 프로토콜 입문 #5-4 SSL 오프로드

자손킴 @jasonkim@hackers.pub

다음에 6장이 있지만 총정리를 하는 챕터이기 때문에 실질적인 내용은 여기까지가 끝이다. 책에서는 SSL 오프로드만 다루고 끝나지만 내부망 안에서의 보안도 짧게 정리했다.

TLS(SSL) 오프로드

TLS를 사용하면 보안이 강화되지만 TLS 핸드셰이크와 암복호화 작업에 CPU 자원을 많이 소모하게 된다. 이것을 전용 장비에 맡기면 웹서버는 효율적으로 애플리케이션 로직 처리에만 신경 쓰면 된다. 특히 로드밸런서에서 중앙집중식으로 TLS를 처리하면 웹서버의 부하가 크게 줄어들고 요청을 빠르게 처리할 수 있게 되어 시스템 전반적으로 부하 분산의 효과가 커진다. 특히 관리가 까다로운 공인 인증서를 로드밸런서에서 집중 관리할 수 있어 운영 부담이 크게 줄어든다.

TLS 오프로드 동작방식

  1. 클라이언트의 HTTPS 요청: 클라이언트가 웹 서비스에 접속하기 위해 HTTPS 요청을 보낸다. 이 시점에서 클라이언트는 TLS 핸드셰이크를 시작한다.

  2. 방화벽(fw1) 통과: 요청이 방화벽 fw1에 도달한다. 방화벽은 허용된 포트(443)로 들어오는 트래픽인지 확인하고, 패킷 필터링 규칙에 따라 트래픽을 통과시킨다. 이 단계에서 트래픽은 여전히 암호화된 상태이므로 일반적인 방화벽은 패킷의 내용을 검사할 수 없고 IP/포트 기반 필터링만 수행한다. (다만 SSL 인스펙션 기능이 있는 차세대 방화벽(NGFW)은 여기서 복호화 후 검사를 수행하기도 한다.)

  3. 로드밸런서(lb1)에서 TLS 종료: 로드밸런서 lb1이 클라이언트와 TLS 핸드셰이크를 완료한다. 서버 인증서를 클라이언트에게 제시하고 세션 키를 협상한 뒤 암호화된 트래픽을 복호화한다. 이 과정을 SSL/TLS Termination이라고 부른다.

  4. 평문 HTTP로 백엔드 전달: 로드밸런서는 복호화된 요청을 분석하여 라우팅 규칙에 따라 백엔드 서버 sv1 또는 sv2에 평문 HTTP로 전달한다. 이때 로드밸런서는 X-Forwarded-For, X-Forwarded-Proto 같은 헤더를 추가하여 원본 클라이언트 정보와 프로토콜 정보를 백엔드에 전달할 수 있다.

  5. 백엔드 서버 처리: 웹서버 sv1 또는 sv2는 평문 HTTP 요청을 받아 애플리케이션 로직을 처리하고 응답을 생성한다. 암복호화 작업이 없으므로 CPU 자원을 온전히 비즈니스 로직에 사용할 수 있다.

  6. 응답 암호화 및 전송: 백엔드 서버의 HTTP 응답이 로드밸런서 lb1로 돌아오면, 로드밸런서는 이를 TLS로 암호화하여 클라이언트에게 전송한다.

망분리는 만병통치약이 아니다.

TLS 오프로드를 하게 되면 로드밸런서를 통과한 패킷은 평문으로 내부망을 돌아다니게 된다. 책을 읽으며 처음에는 "어차피 방화벽 뒤에 있는 내부망이고 외부에서 접근이 차단되어 있으니 평문이어도 괜찮은 거겠군"이라고 생각을 했었다.

그러다 문득 망분리는 만병통치약이 아니라는 트윗이 기억이 났다.

https://x.com/simnalamburt/status/1823610803846517196?s=20

내부망이 안전하다는 가정은 내부자 위협, 자격 증명 탈취를 통한 침해, 그리고 한 시스템이 뚫린 후 내부망을 통해 다른 시스템으로 확산되는 횡적 이동 공격과 같은 위험이 도사리고 있다. 이른바 '침해 가정' 원칙에 따라, 공격자가 이미 망 내부에 들어와 있다는 전제로 보안 체계를 설계해야 한다.

Zero Trust 아키텍처

"결코 신뢰하지 말고 항상 검증하라(Never trust, always verify)"는 원칙에 따라 내부망과 외부망을 구분하지 않고 모든 접근에 대해 인증과 권한 검증을 수행한다. 마이크로 세그멘테이션을 통해 네트워크를 세분화하고 최소 권한 원칙을 적용한다.

mTLS

mTLS는 클라이언트와 서버가 서로의 인증서를 검증하여 양방향으로 신원을 확인하는 상호 인증 방식이다. 일반 TLS는 서버만 인증서를 제시하지만 mTLS에서는 클라이언트도 인증서를 제시해야 한다. Zero Trust 아키텍처에서 mTLS는 핵심 구성요소로 내부 서비스 간 통신에서도 모든 요청의 신원을 검증하여 "항상 검증하라"는 원칙을 기술적으로 구현한다.

그럼 굳이 TLS 오프로드 하지 말고 백엔드까지 암호화된 패킷을 전달하면 되는거 아닌가?

TLS 오프로드는 여전히 유효한 선택이다. L7 로드밸런서가 HTTP 헤더나 URL 경로, 쿠키를 분석해서 트래픽을 라우팅하려면 패킷 내용을 들여다볼 수 있어야 한다. WAF도 SQL 인젝션이나 XSS 같은 공격 패턴을 탐지하려면 평문 상태의 요청을 검사해야 한다. 암호화된 상태로는 이런 기능들이 불가능하다.

결국 L7 기능을 활용하려면 어디선가는 TLS를 종료해야 한다. 이때 공인 인증서 처리를 로드밸런서로 집중시키면 보안 정책 적용과 인증서 갱신이 훨씬 수월해진다.

만약 내부망 보안을 위해 재암호화가 필요한 TLS 브릿징(TLS Bridging) 방식을 사용하더라도 오프로드의 이점은 여전하다. 외부 노출용 공인 인증서는 로드밸런서가 전담하고, 내부 구간은 사설 CA나 자동화된 인증서 발급 체계(mTLS 등)를 이용해 백엔드 서버의 관리 부담을 최소화하면서도 보안과 가시성을 모두 챙길 수 있기 때문이다.

Read more →
1

@hongminhee洪 民憙 (Hong Minhee) I certainly like the focus on growth not punishment. It really aligns with the brief discussion in the " Alternative models of content moderation" sectino of Sarah Gilbert's Towards Intersectional Moderation: An Alternative Model of Moderation Built on Care and Power of justice-based models of moderation that "foster education, rehabilitation, and forgiveness" (as opposed to the more punitive and carceral models of moderation that are the norm in commercial social networks).

My impression is that most fedi moderation teams don't actually notify the reporters about what actions are or aren't taken. Some of that is due to software limitations -- if the report is forwarded from another instance, I'm pretty sure you don't even know who the reporter is (and the Mastodon moderator UX doesn't as far as I know directly support two-way communications, instead you. have to DM the user from the moderator account; and) . Just as importantly, though, reporters sometimes get upset and escalate if action isn't taken. If the original report is malicious, as part of a harassment campaign against the target, this sometimes leads to the moderators getting harassed as well.

Of course it's very frustrating to report something and never hear back so I can certainly see the arguments in favor of doing this as well! But with malicious reporting, or if a team of attackers are reporting different posts to try to understand just what borderline content get through the moderators, there are arguments against it as well. One possibility is to treat reports from people who are part of the HackersPub community (differently on this front than reports from other instances.

While I like the idea of not requiring the reporter to cite the specific clause(s) of the guidelines that are violated, I'm personally skeptical about using LLMs to address the problem. Even assuming the datasets used to train the LLMs were gathered with consent (which isn't typically the case) they're likely to have racial, gender, and cultural biases. Don't get me wrong, the idea of using a tool to help the moderators and report recipient understand just what's been violated is a good one, I'm just not sure this is the right technology. Timnit Gebru and others at DAIR have been thinkiing about approaches to content moderations, so might have some ideas here.

In terms of cross-instance reports, it's really important to leave it up to the reporter whether or not to forward to another instance -- sometimes the admins of the other instance are hostile! And like the discussion of malicious reporting above, this highlights the importance of threat modeling.

Anyhow those are my quick initial thoughts. Looking forward to seeing how the functionality evolves! As you're moving forward with this, it might be interesting to talk to some of the academic researchers who have looked at moderation on the Fediverse -- Sohyeon Hwang, Owen Xingjian Zhang, Tolulope Oshinowo and others at Princeton have done some excellent work and talked to a lot of people here in the process. If it's useful, I can see if they're interested in connecting with you.

@jdp23Jon Thank you so much for this thoughtful feedback and for the paper reference! I'll definitely read Sarah Gilbert's work.

You raise excellent points I hadn't fully considered:

  • On notifying reporters: The malicious reporting scenario is a real concern. I like your suggestion of treating reports from within the community differently from forwarded reports. We should think more carefully about what information to share and with whom.

  • On LLM usage: That's a fair criticism. The bias issue is something we need to take seriously. Perhaps the LLM should only assist moderators as a reference tool, not be exposed to reporters or reported users directly. I'll look into DAIR's work on this.

  • On cross-instance forwarding: Absolutely agreed; this should be opt-in by the reporter, not automatic. I'll update the spec to make that explicit.

I'd love to connect with the Princeton researchers if they're open to it. And thank you for taking the time to share your experience: this is exactly the kind of input we were hoping for.

1

Hi ! I'm working on Hackers' Pub, a small -powered social platform for developers and tech folks.

We're currently drafting a content (/) system and would really appreciate any feedback from those who have experience with federated moderation—we're still learning.

Some ideas we're exploring:

  • Protecting reporter anonymity while giving reported users enough context to understand and improve
  • Graduated responses (warning → content removal → suspension) rather than jumping to bans
  • Using LLM to help match reports to code of conduct provisions
  • Supporting ActivityPub Flag activity for cross-instance reports

Our guiding principle is that moderation should be about growth, not punishment. Expulsion is the last resort.

Here's the full draft if you're curious: https://github.com/hackers-pub/hackerspub/issues/192.

If you've dealt with moderation in federated contexts, what challenges did you run into? What worked well? We'd love to hear your thoughts.

5